Improving Database Design for a Notification System in MySQL - mysql

I'm in the process of building a common notification system for a webapp. The main technologies I'm using are Java, Spring MVC and Hibernate. I've been looking at several posts here and there, trying to come up with the solution that suits me best, taking into account recommended practices.
I've already coded my database tables and would like to receive some feedback in order to
improve my design to avoid big changes while I'm implementing my Java classes. My goal is to make it as complete, scalable and optimal as possible mantaining the complexity to the minimum possible.
Here's my code:
NOTIFICATION SAMPLE:
The #user added a new comment.
The user [USER_ID] [USER_ACTION] a new [OBJECT].
>>> Updates >>>
[05/02/14]
Fields id_recipient and seen removed from notification table. (#Kombajn zbożowy)
New table notification_user created. (#Kombajn zbożowy)
Lowercase identifiers. (#wildplasser)
notification
CREATE TABLE notification (
id_notification BIGINT(20) NOT NULL AUTO_INCREMENT,
id_notification_type BIGINT(20) NOT NULL,
id_action_type BIGINT(20) NOT NULL,
id_sender BIGINT(20) NOT NULL,
created_date TIMESTAMP NOT NULL,
url VARCHAR(300) NULL,
PRIMARY KEY (id_notification),
FOREIGN KEY (id_notification_type) REFERENCES notification _type (id_notification_type),
FOREIGN KEY (id_action_type) REFERENCES action_type (id_action_type),
FOREIGN KEY (id_sender) REFERENCES user (id_user)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
notification_user: so that one notification can be sent to many recipients (users).
CREATE TABLE notification_user (
id_notification BIGINT(20) NOT NULL,
id_recipient BIGINT(20) NOT NULL,
seen TINYINT(1) DEFAULT 0,
PRIMARY KEY (id_notification , id_recipient),
FOREIGN KEY (id_notification) REFERENCES notification (id_notification),
FOREIGN KEY (id_recipient) REFERENCES user (id_user)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
notification_type: refers to the type of object that was modified by the actions of a certain user. Example: comment, post, etc.
CREATE TABLE notification_type (
id_notification_type BIGINT(20) NOT NULL AUTO_INCREMENT,
notification_name VARCHAR(100) NOT NULL,
description VARCHAR(300) NULL,
PRIMARY KEY (id_notification_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
action_type: actions executed by the users which trigger the notifications. Typically: update, add, remove, etc.
CREATE TABLE action_type (
id_action_type BIGINT(20) NOT NULL AUTO_INCREMENT,
action_name VARCHAR(100) NOT NULL,
PRIMARY KEY (id_action_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Related

MySQL Repeatable Read and Phantoms, unique username example

Martin Kleppmann in his book "Designing Data-Intensive Applications" is showcasing the following problem:
Claiming a username
On a website where each user has a unique username, two users may try to create
accounts with the same username at the same time. You may use a transaction to
check whether a name is taken and, if not, create an account with that name.
However, like in the previous examples, that is not safe under snapshot isolation.
Fortunately, a unique constraint is a simple solution here (the second transaction
that tries to register the username will be aborted due to violating the constraint).
I have a very similar use case, where 2 transactions are trying to claim the name of the entity.
At the beginning of each transaction, I run a select to see if such name was already taken. If it wasn't - create or update, depending on the operation requested by the user. This logic crumbles under concurrent attempts to claim/modify the name.
I am trying to see if there is a mechanism that allows implementing correct behavior under the Repeatable Read isolation level. Unique constraint violation thrown by the DB is not acceptable in my case, neither is a downgrade to Serializable execution.
Can I employ Select For ... Update here? Obviously, I won't be locking the concrete rows, but rather an entire table (correct me if I am wrong in my assumption) as I will not have pk index columns in the WHERE subclause?
Table structure:
CREATE TABLE `application_domains` (
`id` varchar(255) NOT NULL,
`name` varchar(255) NOT NULL,
`description` varchar(10000) DEFAULT NULL,
`org_id` varchar(255) NOT NULL,
`created_time` bigint(20) NOT NULL,
`updated_time` bigint(20) NOT NULL,
`created_by` varchar(16) NOT NULL,
`changed_by` varchar(16) NOT NULL,
`revision_id` varchar(16) DEFAULT NULL,
`topic_domain` varchar(255) NOT NULL,
`enforce_unique_topic_names` tinyint(1) NOT NULL DEFAULT '1',
`sample_id` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UK_orgId_name` (`org_id`,`name`),
UNIQUE KEY `UK_orgId_sampleId` (`org_id`,`sample_id`),
KEY `FK_references_application_domains_organization` (`org_id`),
KEY `FK_app_domain_samples_id_references_application_domains_tbl` (`sample_id`),
CONSTRAINT `FK_app_domain_samples_id_references_application_domains_tbl` FOREIGN KEY (`sample_id`) REFERENCES `application_domain_samples` (`id`) ON DELETE SET NULL ON UPDATE SET NULL,
CONSTRAINT `FK_references_application_domains_organization` FOREIGN KEY (`org_id`) REFERENCES `organizations` (`org_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

Is it possible to have a 1 to 0 or 1 recursive relationship and how to query such table?

I have this Ticket table supposed to be used in a Queue system where I can forward a ticket from a Service queue to a different queue.
My Ticket table has fields such as number, ticketRequest (timestamp of when the ticket was created), expectedCallTimestamp (timestamp of predicted call to be attended), etc...
Here's the CREATE SCRIPT for the table Ticket:
CREATE TABLE `ticket` (
`ticket_id` int(11) NOT NULL AUTO_INCREMENT,
`wrong_ticket_id` int(11) DEFAULT NULL,
`number` int(3) NOT NULL,
`forwarded` tinyint(1) NOT NULL DEFAULT '0',
`answered` tinyint(1) DEFAULT NULL,
`ticketRequest` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`expectedCallTimestamp` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
`callTimestamp` timestamp NULL DEFAULT NULL,
`serviceDuration` time DEFAULT NULL,
`service_id` int(11) NOT NULL,
`organic_unit_id` int(11) NOT NULL,
`device_id` int(11) NOT NULL,
`user_id` int(11) DEFAULT NULL,
`counter_id` int(11) DEFAULT NULL,
PRIMARY KEY (`ticket_id`),
UNIQUE KEY `UQ_ticket_wrongTicketId` (`wrong_ticket_id`) USING BTREE,
KEY `FK_ticket_queue` (`service_id`,`organic_unit_id`),
KEY `FK_ticket_device` (`device_id`),
KEY `FK_ticket_workerbycounter` (`user_id`,`counter_id`),
CONSTRAINT `FK_ticket_device` FOREIGN KEY (`device_id`) REFERENCES `device` (`device_id`),
CONSTRAINT `FK_ticket_queue` FOREIGN KEY (`service_id`, `organic_unit_id`) REFERENCES `queue` (`service_id`, `organic_unit_id`),
CONSTRAINT `FK_ticket_workerbycounter` FOREIGN KEY (`user_id`, `counter_id`) REFERENCES `workerbycounter` (`user_id`, `counter_id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=latin1
OK, so what I thought was - when a client picks a different service that he intended to pick (it happens sometimes) I, as the operator behind the counter, can create a new ticket with the same ticketRequest timestamp of the wrongServiceTicket and use that wrongServiceTicket's.ticket_id to reference it through a forwardedTicket's.wrong_ticket_id (the one that has the intended/correct service) for several purposes.
My approach to this is having a 1 to 0 or 1 recursive relationship on the table Ticket, with a nullable & unique field of wrong_ticket_id that is the same as the wrongServiceTicket.ticket_id when a forwardedTicket is created, as previously explained.
Here's a data model example of what I'm trying to build:
Is it possible to have this implementation or is there a better way to handle this problem? And how do I select the info of the wrongServiceTicket when, for example, calling the next ticket in line when having a ticket that's forwarded in the same queue as other tickets that aren't?
As I understood you intend to establish parent-child relationship between existing ticket (wrong one) and new ticket (corrected one).
Your suggested approach is viable except few minor corrections -
Keep wrong_ticket_id column as NULLABLE as it can be null in most of the tickets which were created with a valid service.
You can optionally put a check constraint on wrong_ticket_id. This column will be populated only when 'forwarded' = True.

Why should I create a MySql table containing index only?

I'm following a Java Spring tutorial to learn some basic information about the secure login in a web app.
In this tutorial, the author has created 3 MySql table to manage the authentication:
CREATE TABLE `roles` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`role` varchar(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
CREATE TABLE `users` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`login` varchar(20) NOT NULL,
`password` varchar(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
CREATE TABLE `user_roles` (
`user_id` int(6) NOT NULL,
`role_id` int(6) NOT NULL,
KEY `user` (`user_id`),
KEY `role` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
The table "roles" contains the user role (for example "Admin", "User" etc...).
The table "users" contains the user login and password.
So, I can't understand why the table "user_roles" has been created! The relation between "roles" and "users" is one-to-one, so I could insert an index for these 2 tables and delete the "user_roles"...is it right?
Why should I need to join the tables "users --> user_roles --> roles" instead of "users --> roles" ?
Thanks in advance :)
For a 1:1 relationship, have just one table. (There are exceptions, but I don't see any reason for such here.)
If you have many users in each role but a user is in only one role, then add role_id to the Users table. This is 1:many. You may need INDEX(role_id).
If a user can have many roles and each role can have many users, then you need many:many. And this would be the optimal way to write the 3rd table:
CREATE TABLE `user_roles` (
`user_id` int(6) NOT NULL,
`role_id` int(6) NOT NULL,
PRIMARY KEY (`user_id`, role_id),
KEY (`role_id`, user_id)
) ENGINE=InnoDB DEFAULT;
In some sense, that is an index-only table, since the PRIMARY KEY contains all the fields.
The (6) in INT(6) is meaningless. In particular, it does not give you 6-digit integers, it still gives you 4-byte signed integers up to 2 billion. Perhaps you should use MEDIUMINT UNSIGNED for values of 0..16M. Or SMALLINT UNSIGNED for 2-byte values of 0..64K.

Unique constraint validation order MySql/MariaDb

Just ran into an interesting question about UNIQUE CONSTRAINT validation orders.
Is it possible to tell Mysql(or MariaDB) to validate one unique constraint before the other (id before email)?
CREATE TABLE IF NOT EXISTS users (
`pk` int unsigned NOT NULL AUTO_INCREMENT,
`id` VARCHAR(50) NOT NULL,
`email` VARCHAR(100) NOT NULL,
`name` VARCHAR(100) NOT NULL,
`password` VARCHAR(100),
PRIMARY KEY (pk),
UNIQUE INDEX id (id),
UNIQUE INDEX email (email),
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
If the id constraint fails it is a duplicate submission and if the email fails it's a business failure creating a second user with an already registered email.
Ofcourse it could be solved by searching for it first but this would somehow be smoother.

MySQL implementing tables into database

I have to implement the tasks below:
Task:
At present, the database knows two types of messages:
Messages that a user posts and that are public for anyone and everyone to read
Messages that a user posts and that are non-public. These messages can only be read by users that the posting user has marked as friends.
In this step, you should add a third type of message. This third type of message should be readable by specified recipients only.
This means the database needs to provide the following:
A way of distinguishing between the three types of messages. This involves a change to the Message table.
A way of specifying who the recipients of a particular message are. This will probably require an additional table.
All this must again be achieved with minimal amount of storage, i.e., you must choose the appropriate data types from the MySQL manual. You may assume that the total number of messages that are added over time may reach 1,000,000,000.
Your job is to implement the necessary changes and additional table for this purpose and any keys and foreign key relationships required.
Here are my two tables first : User
CREATE TABLE IF NOT EXISTS `User` (
`user_id` int(10) unsigned NOT NULL auto_increment,
`given_name` varchar(60) default NULL,
`surname` varchar(60) default NULL,
`address` varchar(255) default NULL,
`city_id` int(10) unsigned NOT NULL,
`date_of_birth` datetime default NULL,
`email` varchar(80) default NULL,
PRIMARY KEY (`user_id`),
KEY `ix_user_surname` (`surname`),
KEY `ix_user_given_name` (`given_name`),
KEY `ix_user_name` (`given_name`,`surname`),
KEY `ix_user_date_of_birth` (`date_of_birth`),
KEY `ix_user_email` (`email`),
KEY `ix_user_city_id` (`city_id`)
) ENGINE=InnoDB
2nd table :Message
CREATE TABLE IF NOT EXISTS `Message` (
`message_id` int(10) unsigned NOT NULL auto_increment,
`owner_id` int(10) unsigned default NULL,
`subject` varchar(255) default NULL,
`body` text,
`posted` datetime default NULL,
`is_public` tinyint(4) default '0',
PRIMARY KEY (`message_id`),
KEY `ix_message_owner_id` (`owner_id`)
) ENGINE=InnoDB
MY SOLUTION: I was thinking of creating a new table called 'Message_level' and have columns 'message_level_id'(will refer to 1,2,3 as 1=public, 2=private, 3=specific) & 'message_level'(where it would state public,private and specific next to level). Then I can use the 'Message_level' as a foreign key into the 'Message' table and replace the 'is_public' column with 'message_level_id'.
Is my approach to this question right? is there another way I can do this to make it more efficient?
and how would I approach the second task of specifying who the recipients of a particular message are?
I would go like this:
User: user_id, given_name, ...
Message: message_id, owner_id (fk User), subject, body, posted, message_type_id (fk Message_type)...
Message_recipients: user_id (fk User), message_id (fk Message)
Message_type: message_type_id, description (1:public, 2:friends, 3:specific_recipients)