Error while creating foreign relation in knex - table creation race condition - sql

I'm new to knex and haven't touched RDBMS in years (been in NoSQL land), so bear with me here.
I've got two migration files, one for tracks and one for users (tracks are owned by users). Below are the relevant files:
migrations/20190919103115_users.js
exports.up = function(knex) {
return knex.schema.createTable('users', table => {
table.increments('id');
table.string('email', 50);
table.string('first_name', 50);
table.string('last_name', 50);
}
};
exports.down = function(knex) {
return knex.schema.dropTable('users');
};
migrations/20190406112728_tracks.js
exports.up = function(knex) {
return knex.schema.createTable('tracks', table => {
table.increments('id');
table.string('name', 140).notNullable();
table.integer('owner_id').notNullable();
table
.foreign('owner_id')
.references('id')
.inTable('users')
.onDelete('CASCADE');
table.json('metadata');
});
};
exports.down = function(knex) {
return knex.schema.dropTable('tracks');
};
When I run yarn knex migrate:up, I get:
migration file "20190406112728_tracks.js" failed
migration failed with error: alter table "tracks" add constraint "tracks_owner_id_foreign" foreign key ("owner_id") references "users" ("id") on delete CASCADE - relation "users" does not exist
I find the official Knex documentation to be pretty lacking (it's more of a reference than anything else) and can't figure out what I'm missing. Obviously I need some way for users to be created before tracks, but don't know how.
EDIT:
It seems this is how it's done: https://github.com/tgriesser/knex/issues/938#issuecomment-131491877
But it seems wrong to just put the entire set of tables in a single migration file. I thought the point was to create one migration file per table?

Migration files are sorted by name before execution, so looks like your tracks file name has an earlier date, therefore it runs before creation of users.
just run npx knex migrate:make create_users, and then npx knex migrate:make create_tracks.
it will generate new files with the proper timestamp, copy your code to the new files, delete the old ones :]

Related

How to use raw SQL (DML and DDL) in Symfony projects?

I'm new in programming and I've built projects on xampp. There I created the databases in SQL (MARIADB) in 2 separated files (DML and DDL).
Now, learning Symfony I found that looks like I must use ORM to create the database. Can't I just create DML and DDL and connect/upload them to Symfony instead of using ORM?
I've been 2 days looking for information on how to do that, and I've just found ORM documentation. I wish I just could do something like this function:
function mod001_conectoBD () {
$address= "localhost";
$user= "root";
$password= "";
$database = "projectDDL"; //(which I uploaded on phpmyadmin as projectDDL.sql)
$link = mysqli_connect( $address, $user, $password, $database );
if ( !$link ) {
echo "Fail connection";
}
return $link;
}
function mod001_disconnectBD ( $link ) {
mysqli_close( $link );
}
This ofc is just the example i used on my xampp project. With this I just used the uploaded projectDDL.sql and built the app around it. Thanks and sorry for my ignorance in this matter.
The reason why I want this is building composite Primary keys, and editting id names, which i find so difficult on symfony.
Imagine for example a table that requires 3 foreign keys and its own id to have a 4 fields primary key, dont know how to make that possible in Symfony.
to connect symfony to mariadb you must modify the .env file which is at the root of your project, for your case it will give something like this:
DATABASE_URL="mysql://root#127.0.0.1:3306/projectDDL"
symfony takes care of creating the database for you, and for each table you have to create an entity
you can create your database with this command on the shell:
php bin/console doctrine:database:create
if you want to create a primary key made up of several foreign keys, doctirne allows you to do that I have already used in a project, you create an entity with foreign keys from your tables and you specify at the beginning that it is also an ID, ex :
#[Id, ManyToOne(targetEntity: User::class)]
#[Id, ManyToOne(targetEntity: Comment::class)]
#[Id, ManyToOne(targetEntity: Video::class)]
Documentation:
https://www.doctrine-project.org/projects/doctrine-orm/en/2.14/tutorials/composite-primary-keys.html#use-case-1-dynamic-attributes

How to change datatype of a column in postgresql database using knex migration?

I have a column in a postgresql database table. Currently it is of datatype INTEGER, I want to change it to JSONB datatype.
Maybe this topic can answer your question: Modify column datatype in Knex migration script
For more detail you can have a look at the knex documentation: https://knexjs.org/#Schema-alter
Knex supports you to write alter column as the way you create column. The different is that you have to use alterTable() to get alterTable builder in knex that will support modifing your database information.
Or else you can take a look at my code:
Assume that I have a previous migration run before like this:
export function up(knex) {
return knex.schema.createTable('users', table => {
table.increments('id').primary('id');
table.string('username').notNullable().unique('username');
table.string('fullName');
table.string('email');
table.string('password');
table.timestamps(true, true);
});
}
And I want to modify column email so that I will use alterTable to modify the column. Note: Keep in mind that when you do migration with knex postgresql you may be failed because of some reasons so that you should use transaction for making sure that the failure will not effect to your database. But with migration of mysql, you will no need to use this because knex mysql do support transaction while dealing with migration.
export async function up(knex) {
const transaction = await knex.transaction();
try {
await transaction.schema.alterTable('users', table => {
table.string('email').notNullable().alter();
});
await transaction.commit();
} catch (error) {
await transaction.rollback();
}
}

Use Postgres generated columns in Sequelize model

I have a table where it's beneficial to generate a pre-calculated value in the database engine rather than in my application code. For this, I'm using Postgres' generated column feature. The SQL is like this:
ALTER TABLE "Items"
ADD "generatedValue" DOUBLE PRECISION GENERATED ALWAYS AS (
LEAST("someCol", "someOtherCol")
) STORED;
This works well, but I'm using Sequelize with this database. I want to find a way to define this column in my model definition, so that Sequelize will query it, not attempt to update a row's value for that column, and ideally will create the column on sync.
class Item extends Sequelize.Model {
static init(sequelize) {
return super.init({
someCol: Sequelize.DOUBLE,
someOtherColl: Sequelize.DOUBLE,
generatedValue: // <<<-- What goes here??
});
}
}
How can I do this with Sequelize?
I can specify the column as a DOUBLE, and Sequelize will read it, but the column won't be created correctly on sync. Perhaps there's some post-sync hook I can use? I was considering afterSync to drop the column and re-add it with my generated value statement, but I would first need to detect that the column wasn't already converted or I would lose my data. (I run sync [without force: true] on every app startup.)
Any thoughts, or alternative ideas would be appreciated.
Until Sequelize supports readOnly fields and the GENERATED datatype, you can get around Sequelize with a custom datatype:
const Item = sequelize.define('Item', {
someCol: { type: DataTypes.DOUBLE },
someOtherCol: { type: DataTypes.DOUBLE },
generatedValue: {
type: 'DOUBLE PRECISION GENERATED ALWAYS AS (LEAST("someCol", "someOtherCol")) STORED',
set() {
throw new Error('generatedValue is read-only')
},
},
})
This will generate the column correctly in postgres when using sync(), and prevent setting the generatedValue in javascript by throwing an Error.
Assuming that sequelize never tries to update the field if it hasn't changed, as specified in https://sequelize.org/master/manual/model-instances.html#change-awareness-of-save, then it should work.

Updating entities with primary key and alternate composite key

I have a series of metric snapshot data I am uploading into my database on a daily basis. I take the input and check to determine if it is already in the database, and if it's not I add it. Each record uses a composite key made up of three columns, and also has a primary key.
I have since tried to add logic so that I can optionally force an update on records that already exist in the database, in addition to adding those that don't yet exist. I run into an error though preventing me, saying that there is already an object with the specified key being tracked.
The instance of entity type 'MembershipSnapshot' cannot be tracked
because another instance of this type with the same key is already
being tracked. When adding new entities, for most key types a unique
temporary key value will be created if no key is set (i.e. if the key
property is assigned the default value for its type). If you are
explicitly setting key values for new entities, ensure they do not
collide with existing entities or temporary values generated for other
new entities. When attaching existing entities, ensure that only one
entity instance with a given key value is attached to the context.
Here's a snippet of my code.
// Get the composite keys from the supplied list
var snapshotKeys = snapshots.Select(s => new { s.MembershipYear, s.DataDate, s.Aggregate }).ToArray();
// Find which records already exist in the database, pulling their composite keys
var snapshotsInDb = platformContext.MembershipSnapshots.Where(s => snapshotKeys.Contains(new { s.MembershipYear, s.DataDate, s.Aggregate }))
.Select(s => new { s.MembershipYear, s.DataDate, s.Aggregate }).ToArray();
// And filter them out, so we remain with the ones that don't yet exist
var addSnapshots = snapshots.Where(s => !snapshotsInDb.Contains(new { s.MembershipYear, s.DataDate, s.Aggregate }))
.ToList();
// Update the ones that already exist
var updateSnapshots = snapshots.Where(s => snapshotsInDb.Contains(new { s.MembershipYear, s.DataDate, s.Aggregate }))
.ToList();
platformContext.MembershipSnapshots.AddRange(addSnapshots);
platformContext.MembershipSnapshots.UpdateRange(updateSnapshots);
platformContext.SaveChanges();
How do I go about accomplishing this task?
I don't have a compelling reason why I have an auto-increment primary key, other than perhaps whatever performance issues it might give SQL internally?
EDIT: The way I've currently solved this issue is my removing my surrogate key, which I'm not using at all for anything. Still, it would be nice to know a workaround without having to remove this as a surrogate key could come in handy in the future.

How to clean up referenced files after a cascaded delete in an RDMS?

Zend_Db contains methods to cascade deletes and updates much like the way that most RDMS's will. The documentation states that if you are using a RDMS which supports forign keys that you should use that in place of the support in Zend_Db. The reasons for this are obvious; Zend_Db issues separate db queries for each delete/update so you lose both performance and (depending on transactional concurrency settings) atomicity.
What then is the best option given the following scenario:
A user has multiple albums, an album contains multiple photos. Each photo, album and user has a single row in equivilent db tables, however each photo is also stored on a CDN. If a user is deleted, the database cascade deletes their albums and photos. However the photos remain on the CDN and need to be removed.
maybe it would be possible to overwrite default cascade delete method with my own model's delete method but it souds like a lot of work so i've made a workaround for my project.
i have 'zadania' table and 'zadania_images' table. the second table has 'zadanie_id' column which relates the two tables.
'zadanie_images' rows contain filenames and after cascade delete of parent 'zadanie_id' the files were still there. here's my solution:
in 'Zadanie' model:
public function deleteZadanie($id) {
$row = $this->find($id)->current();
if($row) {
$image = new Model_ZadanieImage();
$image->deleteRelatedFiles($id);
$row->delete();
}
}
in 'ZadanieImage' model:
public function deleteImage($id) {
$row = $this->find($id)->current();
if($row) {
//delete the record
$filename = $row['name'];
$row->delete();
//and delete related files
unlink('images/zadanie/' . $filename);
unlink('images/zadanie/thumbs/' . $filename);
}
}
public function deleteRelatedFiles($zadanie_id) {
$select = $this->select()->where('zadanie=?', $zadanie_id);
$images = $this->fetchAll($select);
foreach($images as $image) {
$this->deleteImage($image->id);
}
}