FabienArr's avatar

How use negative integer as primary key

Hello,

For some reasons that I have a table which can be populated by a desktop application and a laravel application, I need to use negative integer as primary key for some tables. So I can't use auto-increment for theses tables on Laravel. Do you have any idea of which is the best solution to do this and if it's ok for Laravel to work with negative primary key ?

Regards

0 likes
21 replies
Tray2's avatar

Why on earth would you need to do that?

1 like
Glukinho's avatar

You already have tables with negative primary keys? Or you plan to impement it? Can you show table schema?

Why you need specifically primary key? You can have normal unsigned auto-increment PRIMARY key along with another field with negative values, which is used as foreign key to other tables, if you really need that.

FabienArr's avatar

I have 2 applications, the two applications can create of each side new records for a same table. The same table of each applications will be synhcronized, so to identify the sources and to don't have the same ID for 2 differents records, The first application have postiive ID and other the negative, and when we synchronize all will be ok. I don't want to use UUID, because all my synchronisations works with integer. My question is eloquent will be ok with that or not.

Glukinho's avatar

@FabienArr Eloquent relations can work on any IDs (string, integer, uuid - whatever you want) as long as they uniquely represent rows in tables. If you have a custom ID that is out of conventions, you should explicitly set it in relation, for example:

$this->hasMany(RelatedModel::class, 'custom_foreign_key');

Your approach seems strange and unreliable, but it should work.

Why having positive and negative values? If you need to identify source of a row, you can set source column and Enum casting:

// enum
enum ApplicationSource: int
{
	case APP_MAIN      = 1;
	case APP_SECONDARY = 2;
}

// migration
$table->tinyInteger('source');

// model
$casts = [
	'source' => ApplicationSource::class,
];

Then app1 creates rows with source = ApplicationSource::APP_MAIN (or source = 1) and app2 creates rows with source = ApplicationSource::APP_SECONDARY (or source = 2) .

martinbean's avatar

so to identify the sources and to don't have the same ID for 2 differents records, The first application have postiive ID and other the negative, and when we synchronize all will be ok.

@FabienArr And what happens if you get two records with the same negative ID? Or need to support a third producer of records? “Negative” IDs is not the solution. A unique value such as a UUID or ULID is.

Just because you use UUIDs or ULIDs to uniquely identify records coming from different sources does not mean you need to use UUIDs/ULIDs for primary keys, though. You can create a record using a UUID, and when you sync it, it gets assigned the next primary key in the sequence.

I did this for an EPOS project. The EPOS system needed to work offline, so it was impossible to keep IDs in sync because I wouldn’t know what the “latest” ID is without being able to ask the server, and other records may be created by other terminals that did have network connectively whilst one terminal was offline. So, the terminals just created orders with a UUID, and then when it regained network connectivity, would send the orders to the server to be inserted into the database (which used auto-incrementing IDs in its tables).

FabienArr's avatar

I agree with you for the source, but for primary ID of the tableI don't see any other solutions. But my english is not good perhaps.

App_main : mytable : record 1 : ID = 1 //Create from the main application database
App_main : mytable : record 2 : ID = -1 //Create from the laravel application and synchronised in the database of main application

App_laravel : mytable : record 1 : ID = -1 //Create from the laravel application database
App_laravel : mytable : record 2 : ID = 1 //Create from the main application and synchronised in the database of laravel application

I need to synchro records from "mytable" of the two databases in the two applications.

But I need to control the auto increment in negative value alone, because there is nothing to do it automatically I think.

Glukinho's avatar

@FabienArr You want to have non-overlapping primary keys across two tables, right?

This is exact case for UUID (or ULID) as primary key, and Laravel supports it natively: https://laravel.com/docs/12.x/eloquent#uuid-and-ulid-keys

Another approach is auto-incrementing integer keys, but App1 assigns keys 1, 3, 5, 7... and App2 assigns keys 2, 4, 6, 8... As a result, primary keys never overlap. It is done on database level (google for mysql auto_increment_offset) and is used mainly for replication. This approach is more complicated, you will suffer setting it up, supporting and extending further.

In your approach I see at least two bad things:

  1. how would you divide primary keys when you have three applications, not two? Positive keys go to App1, negative go to App2, nothing is left for App3, App4... etc.
  2. Mysql can't produce auto incrementing (decrementing, to be precise) negative primary keys, as far as I know, you have to handle them yourself.

Why complicate your life? Use UUIDs.

FabienArr's avatar

@Glukinh Yes I want non-overlapping primary keys across two tables. Yes I know UUID, but UUID is unreadeable by human, it takes more spaces to store and I need to rewrite my synchronisation's module between the 2 applications because it uses integer. I didn't know mysql auto_increment_offset, I will google that.

For the 2 bad things, I'm agree.

I need to think about UUID, but 26 char for an ID it's quite big

Glukinho's avatar

@FabienArr If your app is so large that you have to estimate UUID column size, then you definitely shouldn't implement "auto-decrementing" negative primary key, reinventing database job by yourself.

1 like
FabienArr's avatar

@Glukinho I think I will use UUID because ULID doesn't exists in my other application. but the problem is that Laravel create UUID with the dash, and my application don't use the dash. Is it possible with Laravel to remove the dash in the database ?

Glukinho's avatar
Level 31

@FabienArr There are two ways:

  1. examine \Illuminate\Database\Eloquent\Concerns\HasUuids trait in Laravel source code and create your own:
trait HasUuidsWithoutDashes
{
    use HasUniqueStringIds;

    public function newUniqueId()
    {
        return (string) Str::of(Str::uuid7())->remove('-');
    }
}

Attach use HasUuidsWithoutDashes; to your model instead of HasUuids.

Note that HasUuids trait has isValidUniqueId() method which is not applicable to your situation (UUID without dashes is not valid UUID for Laravel, as function Str::isUuid($uuid_without_dashes) results in false). I don't know how it affects your app. Maybe you should implement it too:

protected function returnDashes(string $value): string
{
    // in:  019889e6ad48710f9305772d4cd1a0c1
    // out: 019889e6-ad48-710f-9305-772d4cd1a0c1

    $value = Str::of($value);

    return sprintf('%s-%s-%s-%s-%s',
        $value->substr(0, 8),
        $value->substr(8, 4),
        $value->substr(12, 4),
        $value->substr(16, 4),
        $value->substr(20, 12),
    );
}

protected function isValidUniqueId($value): bool
{
	return Str::isUuid($this->returnDashes($value));
}
  1. Second approach is to create cast which removes dashes on set and adds them on get and apply it to your models' UUID fields: https://laravel.com/docs/12.x/eloquent-mutators#custom-casts

Both ways are non-standard, you should test them carefully.

Glukinho's avatar

@FabienArr

the problem is that Laravel create UUID with the dash, and my application don't use the dash

Is it a problem at all? These IDs will be of different size but they still don't overlap as long as your second app produces real UUIDs.

Glukinho's avatar

@FabienArr I mean, leave database field 36 chars long. Let Laravel put there UUID with dashes (36 chars) and your second app - UUID without dashes (32 chars). There is a chance it will work as you expect.

FabienArr's avatar

@Glukinho UUID generated by Laravel 019889c6-0948-72a5-aa81-3708630b2e0e
UUID generated my app : 4ECC362BD992AD4F8E121BBCC2727087

When I integrate the laravel UUID in my app, it is converted automatically like this : 019889C6094872A5AA813708630B2E0E

But if I need to resynchro this record to Laravel app, it will not match Iwith the initial record created in Laravel, It's what I supposed

Glukinho's avatar

@FabienArr Ok, so try to generate UUIDs without dashes in Laravel. Think about letter case too - maybe add ->upper() to a newUniqueId() method.

jaseofspades88's avatar

You could simply have a column called something like source where you have either app or desktop and use that to distinguish the type of entry via an enum. At least that would be using something correctly.

2 likes
Tray2's avatar

I agree with @jaseofspades88, if two applications use the same database, same tables and such, and you want to keep the data separate, you need to add some kind of tenant/application id.

orphanedrecord's avatar

The negative ID is an interesting approach, but I wouldn't recommend that as others like @martinbean and @jaseofspades88 have mentioned. But, to answer your question, yeah it seems like it would work … it's just unorthodox. And if you may be relating records to this table now or in the future, you could end up with negative IDs in other tables too to join to these records? That just seems like a scenario your future self or others would not like to encounter :)

Perhaps instead consider reconciling the IDs in the Laravel app at the time they are merged in from the desktop app (to be unsigned auto-incrementing IDs or UUIDs).

Adding another column to identify the source seems like a good idea to me too. Another alternative would be store a created_by or user_id field to track the User who created a given record. Then, your desktop app could have a different User than your Laravel app?

Please or to participate in this conversation.