Creating related Models on Laravel/Eloquent relations with primary key type UUID - sql

Laravel: 7; PHP: 7.4/8.0
In my project I have two related models: User and TimeAccount. Both use UUID for their primary key, the key name is still id.
Migrations:
class CreateUsersTable extends Migration
{
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->uuid('id')->primary();
// snip...
});
}
// snip...
}
class CreateTimeAccountsTable extends Migration
{
public function up()
{
Schema::create('time_accounts', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('user_id')->nullable()->constrained();
// snip...
});
}
// snip...
}
Models:
class User
{
use IsIdentifiedByUuid;
protected $keyType = 'string';
public function timeAccount()
{
return $this->hasOne(TimeAccount::class);
}
// snip...
}
class TimeAccount
{
use IsIdentifiedByUuid;
protected $keyType = 'string';
public function user()
{
return $this->belongsTo(User::class);
}
// snip...
}
IsIdentifiedByUuid Trait:
trait IsIdentifiedByUuid
{
protected static function bootIsIdentifiedByUuid()
{
static::creating(fn ($model) => static::isIdentifiedByUuidCreatingHandler($model));
static::saving(fn ($model) => static::isIdentifiedByUuidSavingHandler($model));
}
public function initializeIsIdentifiedByUuid()
{
$this->keyType = 'string';
}
protected function getUuidColumn(): string
{
return property_exists($this, 'uuid_column') ? $this->uuid_column : 'id';
}
protected static function getNewUuid(): UuidInterface
{
$columnName = app(static::class)->getUuidColumn();
$tableName = app(static::class)->getTable();
$columnDescriptor = "$tableName.$columnName";
$uuid = "";
$query = \DB::table($tableName)
->select($columnDescriptor)
->where($columnDescriptor, $uuid);
$attempts = 0;
do {
if ($attempts >= \App\Constants\Uuid::MAX_ATTEMPTS) {
throw new UuidMaxGeneratorAttemptsExceededException();
}
$uuid = Str::uuid();
$attempts++;
} while ($query->setBindings([ $uuid->toString() ])->count() > 0);
return $uuid;
}
/**
* Handles the creation of a model.
* - Generates new UUID if the UUID column is empty and auto-increment is enabled
*
* #param Model $model
* #throws \App\Exceptions\UuidMaxGeneratorAttemptsExceededException
*/
protected static function isIdentifiedByUuidCreatingHandler(Model $model)
{
$columnName = $model->getUuidColumn();
if ($model->getIncrementing() && !$model->{$columnName}) {
$uuid = static::getNewUuid();
\Log::debug(
"IsIdentifiedByUuid [CREATING]:" .
" Generating new UUID for `" . get_class($model) . ": $uuid`"
);
$model->{$columnName} = $uuid->toString();
} else {
\Log::debug(
"IsIdentifiedByUuid [CREATING]:" .
" Using existing UUID for `" . get_class($model) . ": $model->{$columnName}`"
);
}
}
/**
* Handles the saving of a Model.
* - Prevents changes to the UUID column
* - Rolls back changed value to old value
*
* #param Model $model
*/
protected static function isIdentifiedByUuidSavingHandler(Model $model)
{
$columnName = $model->getUuidColumn();
$originalUuid = $model->getOriginal($columnName);
if (!is_null($originalUuid) &&
$originalUuid !== $model->{$columnName}
) {
\Log::debug(
"IsIdentifiedByUuid [SAVING]:" .
" Prevented change of UUID for `" . get_class($model) . ":$originalUuid`"
);
$model->{$columnName} = $originalUuid;
}
}
}
Now the Problem:
Within an user-observer on the created hook I'm doing this:
$userTimeAccount = $user->timeAccount()->create([
// snip...
]);
So far - before switching to UUID instead of integer keys - everything worked fine! As far as I know creating the related model before the user-model has been saved (and obviously before auto-inc was triggered) is something that is explicitly allowed by eloquent/Laravel (though I can not find the section of the docs).
After switching to UUID this is not working anymore and I'm getting an SQL error:
SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (laravel_torus.time_accounts, CONSTRAINT time_accounts_user_id_foreign FOREIGN KEY (user_id) REFERENCES users (id)) (SQL: insert into time_accounts (time_account_type_id, balance, borrow, user_id, id, updated_at, created_at) values (3, 0, 0, 0, a995807b-b7e1-4af3-96de-7c48187f943d, 2021-07-09 12:54:25, 2021-07-09 12:54:25))
SQL beautified:
insert into `time_accounts`
(`time_account_type_id`, `balance`, `borrow`, `user_id`, `id`, `updated_at`, `created_at`)
values
(3, 0, 0, 0, 'bad86e70-7496-4b42-b2a1-5cc3c5a4d06c', '2021-07-09 11:19:17', '2021-07-09 11:19:17');
Somehow Laravel tries to insert zero (integer) for the Foreign key which will obviously not work. I even tried setting keyType to 'string' manually without effect.
Where am I wrong?
EDIT: It seems like there is a race condition. The created hook of the Observer will be triggered before the created hook of the User model. This leads to the missing ID - the ID is present when using integer and auto increment, just as it should.

TL;DR
I was not able to solve this with MySQL and switched to Postgres SQL.
DONT USE UUID WITH MYSQL unless you are, absolutely sure that: 1. you need to and 2. the performance impact will be negligible!
I really can not explain how this error is happening at all. The ID in question already exists (looking with the debugger into the $attribnutes array of the model), still Laravel will return integer 0 when calling $user->id.
While investigating the problem I noticed that Laravel is creating String columns for the UUID instead of binary (wich is what I would have expected). Since string comparism is quite cost intensive compared to integer/binary comparism I decided to abandon MySQL. UUID's are mandatory for this project and a shift to another DBMS, away from MySQL, in future was already intended.
Postgres SQL supports a special Column type uuid wich will be used by Laravel if a UUID column is defined. This does not only store and compare the ID as binary but also checks for format and unique values, meaning you can not enter an invalid UUID by accident like in MySQL regular sting columns wich seem to have no checks at all apart from not null and unique.

Related

Alter a column to perform onDelete('restrict') when foreign key is also primary key of self table for Laravel migrations

I have two table (**tbl_Traditional_Policysummery_KYC **& tbl_UnderwritingWorkSheetInfo) both table's primary key is PolicyNo
I want to restrict the delete operation of table **tbl_Traditional_Policysummery_KYC **when tbl_UnderwritingWorkSheetInfo has value with the same PolicyNO
I'm using MSSQL.
I have tried these migrations
$table->string('PolicyNo')->unsigned()->index()->change();
$table->foreign('PolicyNo')
->references('PolicyNo')->on(with(new WorksheetInfo)->getTable())
->onDelete('restrict');
<?php
use App\Models\PolicySummaryKYC;
use App\Models\WorksheetInfo;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddForeignKeyConstaintToKYCTable extends Migration
> {
> /**
Run the migrations.
*
* #return void
*/
public function up()
> {
Schema::table(with(new PolicySummaryKYC)->getTable(), function (Blueprint $table) {
> $table->string('PolicyNo')->unsigned()->index()->change();
> $table->foreign('PolicyNo')
> ->references('PolicyNo')->on(with(new WorksheetInfo)->getTable())
> ->onDelete('restrict');
> });
> }
> /**
Reverse the migrations.
*
* #return void
*/
public function down()
> {
Schema::table(with(new PolicySummaryKYC)->getTable(), function (Blueprint $table) {
> //
> });
> }
> }
This is what you need to set inside your migration up()
Schema::table('tbl_Traditional_Policysummery_KYC', function (Blueprint $table) {
$table->foreign('PolicyNo')
->references('PolicyNo')
->on('tbl_UnderwritingWorkSheetInfo')
->onDelete('restrict');
});
Note:
I presume you already create another migration earlier to tbl_Traditional_Policysummery_KYC where inside you have declare PolicyNo as string and primary key ( $table->string('PolicyNo')->primary(); )
no need to add unsigned() to field type string() , it is meant for integer only, your PolicyNo field is string
because PolicyNo field is already a primary, no need to be indexed
to get table_name , you can use (new App\Models\ModelName)->getTable(), no need to use with(...) outside Model class

Defining foreign key relationships in Go using GORM [duplicate]

I have those two models:
User model:
type User struct {
DBBase
Email string `gorm:"column:email" json:"email"`
Password string `gorm:"column:password" json:"-"`
}
func (User) TableName() string {
return "t_user"
}
User info model:
type UserInfo struct {
User User `gorm:"foreignkey:u_id;association_foreignkey:id"`
UID uint `gorm:"column:u_id" json:"-"`
FirstName string `gorm:"column:first_name" json:"first_name"`
LastName string `gorm:"column:last_name" json:"last_name"`
Phone string `gorm:"column:phone" json:"phone"`
Address string `gorm:"column:address" json:"address"`
}
func (UserInfo) TableName() string {
return "t_user_info"
}
and I want to make UID related to the id of the user table.
this is the function that creates the user:
func (dao *AuthDAO) Register(rs app.RequestScope, user *models.User, userInfo *models.UserInfo) (userErr error, userInfoErr error) {
createUser := rs.Db().Create(&user)
userInfo.UID = user.ID
createUserInfo := rs.Db().Create(&userInfo)
return createUser.Error, createUserInfo.Error
}
I did try what gorm wrote on the documentation, but without success:
http://doc.gorm.io/associations.html
Note!
from gorm 2.0 this is no longer necessary, read more here:
gorm.io/docs/belongs_to.html#FOREIGN-KEY-Constraints
The solution is to add this line when migrating the database:
db.Model(&models.UserInfo{}).AddForeignKey("u_id", "t_user(id)", "RESTRICT", "RESTRICT")
migration (gorm documentation)
I found the following code correctly created the foreign key without having to do any custom migration code; just the usual AutoMigrate.
type Account struct {
ID uint `gorm:"primary_key"`
}
type Transaction struct {
ID uint `gorm:"primary_key"`
AccountID uint
Account Account `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
}
I am using "Gorm 2.0" which is a dependency of gorm.io/gorm v1.23.3.
Read about the belongs to relationship in https://gorm.io/docs/belongs_to.html
Also, there's a good example here: https://medium.com/#the.hasham.ali/how-to-use-uuid-key-type-with-gorm-cc00d4ec7100
// User is the model for the user table.
type User struct {
Base
SomeFlag bool `gorm:"column:some_flag;not null;default:true"`
Profile Profile
}// Profile is the model for the profile table.
type Profile struct {
Base
Name string `gorm:"column:name;size:128;not null;"`
UserID uuid.UUID `gorm:"type:uuid;column:user_foreign_key;not null;"`
}
We can add foreign key constraints in the latest version using CreateConstraint.
Example:
Suppose we have two entity
type User struct {
gorm.Model
CreditCards []CreditCard
}
type CreditCard struct {
gorm.Model
Number string
UserID uint
}
Now create database foreign key for user & credit_cards
db.Migrator().CreateConstraint(&User{}, "CreditCards")
db.Migrator().CreateConstraint(&User{}, "fk_users_credit_cards")
which translates to the following SQL code for Postgres:
ALTER TABLE `credit_cards` ADD CONSTRAINT `fk_users_credit_cards` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)
Referrence:
Gorm official doc for Constraint Creation
Changes in the latest version
For recent release of Gorm i.e. 1.21.xIf you have a one to one mapping of fields and not have parent to child mapping, here is what you will do
type BaseConfig struct {
ID uuid.UUID `gorm:"primary_key" json:"id"`
}
type Ledger struct {
BaseConfigId uuid.UUID `json:"base_config_id"`
BaseConfig BaseConfig `gorm:"column:base_config_id;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}
In Migration script you will need to follow as per docs
WriteClient.Migrator().CreateConstraint(&models.BaseConfig{}, "Ledgers")
WriteClient.Migrator().CreateConstraint(&models.BaseConfig{}, "fk_base_configs_id")
I feel the tripping point is the missing documentation for how to associate. Some juggling around could be there to figure it out, so hope my answer saves some time.

How do i execute SQL statements from an SQL file in Scheduler in TYPO3?

The concept:
I have a task which imports some data on the database. The database schema can be changed at any time since it is exported to a folder. So i have to read the database file and create the structure. I would like to do it with TYPO3 API. Before TYPO3 9 i would do something like that:
$sqlQueries = explode(';', file_get_contents(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'myFile.sql'));
foreach ($sqlQueries as $sqlQuery) {
$sqlQuery = trim($sqlQuery);
if (!empty($sqlQuery) && $this->db instanceof DatabaseConnection && method_exists($this->db, 'sql_query')) {
$this->db->sql_query($sqlQuery);
}
}
How do i do that with TYPO3 10?
Best regards
TYPO3 10 has already such functionality and you can find it in the maintenance module unter the
Analyze Database Structure. Thanks to Mathias Brodala i found my solution. (It can always be improved).
In your task you call the SqlReader class in order to extract the statements from the SQL file. The use namespace is the following (As of TYPO3 10)
use TYPO3\CMS\Core\Database\Schema\SqlReader;
Now we have two problems with that
The SQL evaluated and executed by TYPO3 is limited to schemas. For content changes you need a 3rd party library/tool.
It only creates tables and doesn't drop them by default.
The first "problem" it says that you can only perform database structure changes but not the content in it. For that, one could use the QueryBuilder.
The second problem is that the getCreateTableStatementArray function in the SqlReader class does the following:
return $this->getStatementArray($dumpContent, '^CREATE TABLE');
This calls the getStatementArray function and it only returns the CREATE TABLE commands. So if you have a DROP TABLE command in the SQL file, it won't be taken into consideration. For that, we need to create our own function for the getCreateTableStatementArray. So in your Task, create this function:
/**
* #param string $dumpContent
* #return array
*/
public function getCreateTableStatementArray(string $dumpContent): array
{
$sqlReader = GeneralUtility::makeInstance(SqlReader::class);
return $sqlReader->getStatementArray($dumpContent);
}
With this, TYPO3 will not evaluate the statements by command name and it will get all the commands available in the sql file.
Now that we have done that, we need to pass the statements into our custom function to be parsed. So somewhere in your code you get file's content. I have it like this:
/**
* #return bool
* #throws \Exception
*/
public function execute()
{
...
$this->createTableStructure();
...
}
protected function createTableStructure()
{
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$sqlStatements = $this->getCreateTableStatementArray(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'myFile.sql'));
foreach ($sqlStatements as $sqlStatement) {
if (strpos($sqlStatement, ' CHARSET=utf8') !== false) {
$sqlStatement = str_replace(" DEFAULT CHARSET=utf8", "", $sqlStatement);
}
$connection = $connectionPool->getConnectionByName('Default');
try {
$connection->executeUpdate($sqlStatement);
} catch (DBALException $e) {
//Your log here
}
}
}
For this part i got an error that TYPO3 could not read the DEFAULT attribute so i had to remove it.
if (strpos($sqlStatement, ' CHARSET=utf8') !== false) {
$sqlStatement = str_replace(" DEFAULT CHARSET=utf8", "", $sqlStatement);
}
This looks like this in the SQL file:
DROP TABLE IF EXISTS `myTable`;
CREATE TABLE IF NOT EXISTS `myTable` (
`myColumn1` int(10) NOT NULL DEFAULT '0',
`myColumn2` varchar(255) DEFAULT NULL,
`myColumn3` varchar(255) DEFAULT NULL,
`myColumn4` date DEFAULT NULL,
`myColumn5` varchar(255) DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Hope it was helpful!
Best regards

symfony2 doctrine multiple update / transaction

I'm working on a Symfony2 project with doctrine and have some problems with a SQL update. I have a database table where all entries have a position attribute, since the order is important and changeable by the customer. Finally, I decided to save the position as simple integer values, without any implemented logic, and update any affected rows.
So, I wrote this function
public function swapCookingStepAction($recipeId, $posBefore, $posAfter) {
$em = $this->getDoctrine()->getManager();
$swappedEntry = $em->getRepository('BarraFrontBundle:CookingStep')->findOneBy(array('recipe'=>$recipeId, 'position'=>$posBefore));
$swappedEntry->setPosition($posAfter);
if ($posBefore < $posAfter)
$steps = $em->getRepository('BarraFrontBundle:CookingStep')->decreaseBetweenPos($recipeId, $posBefore+1, $posAfter);
else
$steps = $em->getRepository('BarraFrontBundle:CookingStep')->increaseBetweenPos($recipeId, $posAfter, $posBefore-1);
$em->flush();
return new Response("ok?");
}
and this repository with doctrine
class CookingStepRepository extends EntityRepository {
public function increaseBetweenPos($recipeId, $posBefore, $posAfter)
{
$query = $this->createQueryBuilder('c')
->update()
->set('c.position', 'c.position +1')
->where('c.recipe = :recipeId')
->andWhere('c.position BETWEEN :posBefore AND :posAfter')
->setParameter('posBefore', $posBefore)
->setParameter('posAfter', $posAfter)
->setParameter('recipeId', $recipeId)
->getQuery();
return $query->getResult();
}
public function decreaseBetweenPos($recipeId, $posBefore, $posAfter)
{
$query = $this->createQueryBuilder('c')
->update()
->set('c.position', 'c.position -1')
->where('c.recipe = :recipeId')
->andWhere('c.position BETWEEN :posBefore AND :posAfter')
->setParameter('posBefore', $posBefore)
->setParameter('posAfter', $posAfter)
->setParameter('recipeId', $recipeId)
->getQuery();
return $query->getResult();
}}
The queries are build as wished but unfortunately I get this primary key error
An exception occurred while executing 'UPDATE CookingStep SET position = position - 1 WHERE recipe = ? AND (position BETWEEN ? AND ?)' with params ["6", 2, "2"]:
Integrity constraint violation: 1062 Duplicate entry '1-6' for key 'PRIMARY'
for params (6, 1, 2). I tried to work with a transaction but this didn't change anything.
Could you help me, guys?
UPDATE
Just added a 4th parameter at the repository function and could remove the 2nd function.
If I change the primary key for tests, the code & update works very well.

Using boolean fields with Magento ORM

I am working on a backend edit page for my custom entity. I have almost everything working, including saving a bunch of different text fields. I have a problem, though, when trying to set the value of a boolean field.
I have tried:
$landingPage->setEnabled(1);
$landingPage->setEnabled(TRUE);
$landingPage->setEnabled(0);
$landingPage->setEnabled(FALSE);
None seem to persist a change to my database.
How are you supposed to set a boolean field using magento ORM?
edit
Looking at my database, mysql is storing the field as a tinyint(1), so magento may be seeing this as an int not a bool. Still can't get it to set though.
This topic has bring curiosity to me. Although it has been answered, I'd like to share what I've found though I didn't do intense tracing.
It doesn't matter whether the cache is enabled / disabled, the table schema will be cached.
It will be cached during save process.
Mage_Core_Model_Abstract -> save()
Mage_Core_Model_Resource_Db_Abstract -> save(Mage_Core_Model_Abstract $object)
Mage_Core_Model_Resource_Db_Abstract
public function save(Mage_Core_Model_Abstract $object)
{
...
//any conditional will eventually call for:
$this->_prepareDataForSave($object);
...
}
protected function _prepareDataForSave(Mage_Core_Model_Abstract $object)
{
return $this->_prepareDataForTable($object, $this->getMainTable());
}
Mage_Core_Model_Resource_Abstract
protected function _prepareDataForTable(Varien_Object $object, $table)
{
$data = array();
$fields = $this->_getWriteAdapter()->describeTable($table);
foreach (array_keys($fields) as $field) {
if ($object->hasData($field)) {
$fieldValue = $object->getData($field);
if ($fieldValue instanceof Zend_Db_Expr) {
$data[$field] = $fieldValue;
} else {
if (null !== $fieldValue) {
$fieldValue = $this->_prepareTableValueForSave($fieldValue, $fields[$field]['DATA_TYPE']);
$data[$field] = $this->_getWriteAdapter()->prepareColumnValue($fields[$field], $fieldValue);
} else if (!empty($fields[$field]['NULLABLE'])) {
$data[$field] = null;
}
}
}
}
return $data;
}
See the line: $fields = $this->_getWriteAdapter()->describeTable($table);
Varien_Db_Adapter_Pdo_Mysql
public function describeTable($tableName, $schemaName = null)
{
$cacheKey = $this->_getTableName($tableName, $schemaName);
$ddl = $this->loadDdlCache($cacheKey, self::DDL_DESCRIBE);
if ($ddl === false) {
$ddl = parent::describeTable($tableName, $schemaName);
/**
* Remove bug in some MySQL versions, when int-column without default value is described as:
* having default empty string value
*/
$affected = array('tinyint', 'smallint', 'mediumint', 'int', 'bigint');
foreach ($ddl as $key => $columnData) {
if (($columnData['DEFAULT'] === '') && (array_search($columnData['DATA_TYPE'], $affected) !== FALSE)) {
$ddl[$key]['DEFAULT'] = null;
}
}
$this->saveDdlCache($cacheKey, self::DDL_DESCRIBE, $ddl);
}
return $ddl;
}
As we can see:
$ddl = $this->loadDdlCache($cacheKey, self::DDL_DESCRIBE);
will try to load the schema from cache.
If the value is not exists: if ($ddl === false)
it will create one: $this->saveDdlCache($cacheKey, self::DDL_DESCRIBE, $ddl);
So the problem that occurred in this question will be happened if we ever save the model that is going to be altered (add column, etc).
Because it has ever been $model->save(), the schema will be cached.
Later after he add new column and "do saving", it will load the schema from cache (which is not containing the new column) and resulting as: the data for new column is failed to be saved in database
Delete var/cache/* - your DB schema is cached by Magento even though the new column is already added to the MySQL table.