Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
December 27, 2020 04:40 pm GMT

Add users table with unique & foreign key constraints in PostgreSQL

Hi guys, welcome back to the backend master class!

So far weve implemented several features for our simple bank system such as create, update, retrieve, or transfer money between bank accounts.

However, theres one very important feature thats still missing: user authentication and authorization. Our banking system could not be completed until this feature is implemented.

So today, were gonna take the first step to implement it, which is adding a new users table to the database and link it with the existing accounts table via some db constraints.

Here's:

Add table users

Alright, so this is the current schema of our simple bank database written using dbdiagram.io

Alt Text

Table accounts as A {  id bigserial [pk]  owner varchar [not null]  balance bigint [not null]  currency varchar [not null]  created_at timestamptz [not null, default: `now()`]  Indexes {    owner  }}Table entries {  id bigserial [pk]  account_id bigint [ref: > A.id, not null]  amount bigint [not null, note: 'can be negative or positive']  created_at timestamptz [not null, default: `now()`]  Indexes {    account_id  }}Table transfers {  id bigserial [pk]  from_account_id bigint [ref: > A.id, not null]  to_account_id bigint [ref: > A.id, not null]  amount bigint [not null, note: 'must be positive']  created_at timestamptz [not null, default: `now()`]  Indexes {    from_account_id    to_account_id    (from_account_id, to_account_id)  }}
Enter fullscreen mode Exit fullscreen mode

At the moment, the accounts table has an owner field to tell us whom this account belong to. So we can use this field as a foreign key to link to the new users table that were going to create.

Lets define Table users with an alias U.

The first field of this table is a username of type varchar. Every user should have a unique username, so this field can serve as the primary key of this table.

Next, we have the hashed_password field to store the hash value of the users password. Its type is also varchar, and it should be not null.

Why is it hashed_password and not just password? Well, basically we should never store a naked password in the database, because if we do then everyone who has access to the database will see the passwords of all users, which is a very critical security issue.

We will learn more about how to store and verify password in another lecture. For now, lets just focus on adding this new users table.

Table users as U {  username varchar [pk]  hashed_password varchar [not null]  full_name varchar [not null]  email varchar [unique, not null]  password_changed_at timestamptz [not null, default: '0001-01-01 00:00:00Z']  created_at timestamptz [not null, default: `now()`]}
Enter fullscreen mode Exit fullscreen mode

OK, the next field will store the full_name of the user, so it is also of varchar type, and should not be null.

One more important field the users table should have is email. We will use it later to communicate with the users, for example, when they forget their password and want to reset it. So the type of this field should also be varchar. And note that it must be unique and not null, since we dont want to have 2 users with the same email.

Then just like other tables, we will have a created_at field to store the time this user is created. This field is not null, and it has a default value of now(), so Postgres will automatically fill in the current timestamp when the new user record is inserted.

Moreover, for security reason, its often a good idea to ask users to change their password frequently, like once every month for instance. So we will need a field: password_changed_at to know when was the last time user changed their password. Its type should be timestamp with timezone, and should also be not null.

The reason I want every field to be not null is because it makes our developers life much easier since we dont have to deal with null pointers.

If the password has never been changed, we will just use a default value which is a long time in the past. As were using golang, Im gonna use a zero value timestamp of Go here. So it should be year 1, month 1, date 1, at 0 hour, 0 minute, 0 second, and the timezone is UTC: '0001-01-01 00:00:00Z'. The letter Z here means zero timezone.

OK so thats the definition of the new users table.

Add foreign key constraint

Now keep in mind that we want to allow 1 user to have multiple accounts with different currencies, so what Im gonna do is to link the owner field of the accounts table to the username field of users table. That would make the owner field become a foreign key.

Here in the definition of the owner, Im gonna add a reference tag that points to U.username. To remind you, U is just an alias of the users table.

Table accounts as A {  id bigserial [pk]  owner varchar [ref: > U.username, not null]  balance bigint [not null]  currency varchar [not null]  created_at timestamptz [not null, default: `now()`]  Indexes {    owner  }}
Enter fullscreen mode Exit fullscreen mode

Now in the diagram, we can see that theres a new link between the username field of users table and the owner field of the accounts table.

Alt Text

The number 1 and character * at the end of this link tell us that this is a 1-to-many relationship, which basically means, 1 user can have multiple accounts, but 1 account can only belong to exactly 1 single user.

Add unique constraint

Theres one more thing we should pay attention to here. We allow 1 user to have multiple accounts, but those accounts should have different currencies. For example, you can have 1 USD account and 1 EUR account, but clearly should not have 2 different USD accounts.

One way to set this constraint at the database level is to add a composite unique index to the accounts table. This index is composed of 2 fields: owner and currency. Thats why its called composite index - an index that involves more than 1 field.

Table accounts as A {  id bigserial [pk]  owner varchar [ref: > U.username, not null]  balance bigint [not null]  currency varchar [not null]  created_at timestamptz [not null, default: `now()`]  Indexes {    owner    (owner, currency) [unique]  }}
Enter fullscreen mode Exit fullscreen mode

Export to PostgreSQL

OK so now our new schema is ready. Lets export it to PostgreSQL.

Here we can see the code to create users table:

CREATE TABLE "users" (  "username" varchar PRIMARY KEY,  "hashed_password" varchar NOT NULL,  "full_name" varchar NOT NULL,  "email" varchar UNIQUE NOT NULL,  "password_changed_at" timestamptz NOT NULL DEFAULT '0001-01-01 00:00:00Z',  "created_at" timestamptz NOT NULL DEFAULT (now()));
Enter fullscreen mode Exit fullscreen mode

Then the alter table command to add foreign key constraint to the owner field:

ALTER TABLE "accounts" ADD FOREIGN KEY ("owner") REFERENCES "users" ("username");
Enter fullscreen mode Exit fullscreen mode

And a composite unique index for the owner and currency.

CREATE UNIQUE INDEX ON "accounts" ("owner", "currency");
Enter fullscreen mode Exit fullscreen mode

Add new schema change to our project

Next, we have to add these new changes to our simple bank project.

One way to do that is to replace the whole content of the init_schema migration file with the new one, then reset the database and rerun the migrate up command.

However, in a real-world project, its not the right way go. Why?

Because requirements change all the time, and it might come after the first version of our system is deployed to the production. And once we have data in the production DB, we cannot reset it to rerun the old migration.

So the right way to apply new schema change is to create a new migration version. Lets open the terminal to generate a new migration.

We have learned how to do this in lecture 3 of this course. But if you dont remember the command, just run:

migrate -help
Enter fullscreen mode Exit fullscreen mode

To create a new migration, we run this command:

migrate create -ext sql -dir db/migration -seq add_users
Enter fullscreen mode Exit fullscreen mode

We use some parameters to tell migrate to set the the output file extension to sql, the output directory to db/migration, use a sequential number as the file name prefix, and the migration name is add_users.

As you can see here, 2 migration files has been generated inside the db/migration folder:

Alt Text

Lets implement them in visual studio code.

Implement the up migration

Im gonna start with the up migration.

First, we need to create table users. So lets copy the SQL query that dbdiagram has generated for us and paste it to this migration file 000002_add_users.up.sql

Next, Im gonna copy the query that adds a new foreign key constraint to the owner field of accounts table. And finally, the query to create a unique composite index for the owner and currency as well.

CREATE TABLE "users" (  "username" varchar PRIMARY KEY,  "hashed_password" varchar NOT NULL,  "full_name" varchar NOT NULL,  "email" varchar UNIQUE NOT NULL,  "password_changed_at" timestamptz NOT NULL DEFAULT('0001-01-01 00:00:00Z'),    "created_at" timestamptz NOT NULL DEFAULT (now()));ALTER TABLE "accounts" ADD FOREIGN KEY ("owner") REFERENCES "users" ("username");CREATE UNIQUE INDEX ON "accounts" ("owner", "currency");
Enter fullscreen mode Exit fullscreen mode

This would be good enough for our new migration up script. However, Im gonna show you another way to ensure that each owner has at most 1 account for a specific currency.

Instead of using a direct unique index like this, we can add a unique constraint for the pair of owner and currency on the accounts table. It will be very similar to the command to add foreign key constraint above:

-- CREATE UNIQUE INDEX ON "accounts" ("owner", "currency");ALTER TABLE "accounts" ADD CONSTRAINT "owner_currency_key" UNIQUE ("owner", "currency");
Enter fullscreen mode Exit fullscreen mode

Basically, under the hood, adding this unique constraint will automatically create the same unique composite index for owner and currency as the command we wrote above.

Postgres will need that index to check and enforce the unique constraint faster. So you can choose either of these 2 commands, whatever you like.

Run the migration up

OK, now lets open the terminal and run make migrateup to apply this new migration.

Alt Text

Oops, weve got an error. And the reason is that the foreign key constraint is violated. Why?

Well, thats because we already have some existing rows in the accounts table, but their owner field is completely random and doesnt link to any existed user. Of course, since the users table doesnt exist until now.

So in this case, we have to clean up all the existing data before running migrate up. This is possible because our existing system is not ready to deploy to production yet.

But note that, as the previous migrate up run was failed, it will change the current schema migration to version 2 but in a dirty state.

Alt Text

So now if we run make migratedown with the purpose of cleaning up the data, we will get an error because of this dirty version.

Alt Text

To fix this, Im gonna manually update the value of this dirty field to false, save it, and go back to the terminal to run make migratedown.

This time the migrations run successfully, and all tables in our database are gone. We can now run make migrateup again to get them back.

Alt Text

OK, the migrations are successful. And back to TablePlus, we can see the new users table. Lets click on the Structure button to see its schema.

Alt Text

It has all the fields that we declared in the code: username, hashed_password, full_name, email.

The password_changed_at field has the correct default value, and similar for the created_at field.

Look at the bottom, there are 2 BTREE indexes:

  • One is for the primary key username, which should be unique.
  • And the other is for the email, which is a unique index as well.

Lets check the accounts table.

Alt Text

Here in the owner field, we can see that it now has a foreign key constraint that links to the username field of the users table.

And at the bottom, theres a new unique index for the owner and currency pair.

So our migration up script works perfectly! Now lets go back to the code and complete the migration down.

Implement the migration down

When writing the migration down, we should reverse what was done in the migration up.

So first we have to drop the unique constraint for the owner and currency pair of the accounts table. The command is:

ALTER TABLE IF EXISTS "accounts" DROP CONSTRAINT IF EXISTS "owner_currency_key";
Enter fullscreen mode Exit fullscreen mode

Next, we have to drop the foreign key constraint for the owner field in a similar way. But now how can we know the name of this foreign key constraint?

Well, its very easy! Lets go back to Table Plus and click on the Info button at the bottom of the window.

Alt Text

Here in the table definition, we can see the foreign key constraint name: accounts_owner_fkey

Lets copy it, and paste it to this command:

ALTER TABLE IF EXISTS "accounts" DROP CONSTRAINT IF EXISTS "accounts_owner_fkey";
Enter fullscreen mode Exit fullscreen mode

The last step we should do is to drop the users table. So lets add this command to the file:

DROP TABLE IF EXISTS "users";
Enter fullscreen mode Exit fullscreen mode

And thats it! The 000002_add_users.down.sql file is done. How can we test if it works or not?

Test the up and down migrations

At the moment, in the Makefile, we only have one make migratedown command to run all the migration down versions. But in this case, we only want to run 1 last migration down version.

So lets add a new make command for this purpose. Im gonna called it migratedown1. The migrate command would be the same as before, except that we need to add one more argument at the end.

migratedown1:  migrate -path db/migration -database "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable" -verbose down 1
Enter fullscreen mode Exit fullscreen mode

The number 1 here means that we only want to rollback 1 last migration, or more precisely, just run the last down migration version that was applied before.

Similarly, I will duplicate the migrateup command, and add a new migrateup1 command that will only applies 1 next migration version from the current one:

migrateup1:  migrate -path db/migration -database "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable" -verbose up 1
Enter fullscreen mode Exit fullscreen mode

Alright, now lets add these 2 new commands to the PHONY list:

.PHONY: postgres createdb dropdb migrateup migratedown migrateup1 migratedown1 sqlc test server mock
Enter fullscreen mode Exit fullscreen mode

And go back to the terminal and run:

make migratedown1
Enter fullscreen mode Exit fullscreen mode

The migration is successful. Now in TablePlus, we can see the current version has been changed to 1.

Alt Text

The users table is gone, and in the accounts table, theres no more foreign key constraint for the owner column, as well as the unique constraint for the owner and currency pair.

Alt Text

So the migration down script worked!

OK, now lets run this command to update the schema to the latest version.

make migrateup1 
Enter fullscreen mode Exit fullscreen mode

As you can see, the new constraints in the accounts table are back.

Alt Text

The current migration version is 2, and the new users table is here as expected.

Alt Text

So today we have successfully added a new users table to the simple bank database.

And while doing so, we also learned how to add some foreign key and unique constraints to build up the relationship between tables and to ensure the consistency of the data.

In the next lectures, were gonna update our golang code to work with this new table, and then add more feature to authenticate and authorize users.

Thank you for reading, and Ill see you guys in the next one very soon!

If you like the article, please subscribe to our Youtube channel and follow us on Twitter or Facebook for more tutorials in the future.


Original Link: https://dev.to/techschoolguru/add-users-table-with-unique-foreign-key-constraints-in-postgresql-1i29

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To