Building a relation between tables - mysql

I need a searchable database with messages, which I can attach tags to, and see whether they have been used, when and where.
For example, I got the following message "Hi! I live in Stockholm and are looking for a handyman. I saw in your profile that you own a toolbox and since I don't own any tools myself, except for a screwdriver, I was hoping that hiring you would be the best since yo can bring your own tools! Please contact me ASAP!"
To this message, I want to attach the tags "Stockholm, handyman, toolbox, screwdriver and tools".
When searching the database, I wish to be able to find all messages containing the tags "Stockholm" and "Toolbox".
If I then decide to use this message above, and use it, I want to be able to set that it was used 2018-02-11 11.52 using the name "John Doe" at the site "findahandyman.site".
Now, this is all fictional, I will use completely different messages with other tags, places etc. But the scenario is real. However, I am not sure what way to do this best would be.
I am thinking like this:
tbl-tags
----------
|id | tag |
----------
tbl-messages
--------------
| id | message |
--------------
tbl-used
-------------------------
| id | date | name | site |
-------------------------
And then build a view where I can search the messages, registered with the tags #1 #2 #3 etc.
Am I thinking right? If I am, how can I relate them all and how to build the view. If I am not, how should I think? And also, how to relate them all and build the view according to your suggestion(s)?

In my opinion you would need to do this:
1.) make the parent tables like this:
create table tbl_tags
(
tagName VARCHAR(50) NOT NULL,
dateAdded datetime NULL,
primary key(tagName)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
create the tbl_message table using an id as a primary key (tagName is here primary because this way tag names will not duplicate) like this:
create table tbl_messages
(
message_ID INT(11) NOT NULL AUTO_INCREMENT,
message text NOT NULL,
dateAdded NULL,
primary key(message_ID)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
For the tbl_used I would make it a mapping table with three columns. One column would be the message_ID (a foreign key from the table tbl_messages) and the other the date and time it was used, I would also add an id as primary here to avoid getting an error if multiple users try to use the same message at the same time.
create table tbl_used
(
used_ID INT(11) NOT NULL AUTO_INCREMENT,
message_ID INT(11) NOT NULL,
timeOfUse dateTime NOT NULL,
PRIMARY KEY (`used_ID`),
FOREIGN KEY (`message_ID`) REFERENCES `tbl_messages` (`message_ID`) ON UPDATE CASCADE
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
2.) create another mapping table to relate the messages and tags tables to each other:
create table tbl_messages_x_tbl_tags
(
message_ID INT(11) NOT NULL,
tagName VARCHAR(50) NOT NULL,
PRIMARY KEY (`message_ID`, `tagName`),
FOREIGN KEY (`message_ID`) REFERENCES `tbl_messages` (`message_ID`) ON UPDATE CASCADE,
FOREIGN KEY (`tagName`) REFERENCES `tbl_tags` (`tagName`) ON UPDATE CASCADE
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
You will notice that you will be unable to populate the foreign key columns in the mapping tables with arbitrary content. You can only insert valid values from their respective parent tables. That means your mapping table data is consistent.
To fill the tables, you first need to fill the parent tables (tbl_messages, tbl_tags), then you can populate the mapping tables (tbl_messages_x_tbl_tags, tbl_used).
On insertion of a new message you would simply check for new tags and insert new tags into the table tbl_tags if they are not already there. Then add the message into tbl_messages and populate the mapping table tbl_messages_x_tbl_tags with (message_ID, tagName) rows.
After that, on each use of the message you can simply write to the database:
mysqli_query($connection, "INSERT INTO tbl_used (message_ID,timeOfUse) VALUES($msgID, NOW())");

tbl-tags
----------
|id | tag |
----------
tbl-message-tags
----------------------
| id | tag_id | msg_id |
----------------------
tbl-messages
--------------
| id | message |
--------------
tbl-used
-------------------------
| id | date | name | site |
-------------------------
Creating tables (if you want, you can add constraints):
create table tbl_tags(id mediumint not null auto_increment, tag varchar(255) not null, primary key(id));
create table tbl_messages(id mediumint not null auto_increment, message text not null, primary key(id));
create table tmt(tag_id mediumint not null, msg_id mediumint not null, primary key(tag_id, msg_id));
Insert some test data:
insert into tbl_tags(tag) values ('tag0'), ('tag1');
insert into tbl_messages(message) values ('msg1'), ('msg2'), ('msg3'), ('msg4'), ('msg5');
insert into tbl_message_tags(tag_id, msg_id) values (1, 1), (0, 1), (1, 2), (0, 3);
After this you can make query like this:
select tag from tbl_tags join (select tag_id from tbl_messages join tbl_message_tags on id = msg_id where msg_id = 1) as t on id = t.tag_id;
Result will be:
----------
| id | tag |
|----|-----|
| 1 | tag0|
|----|-----|
| 2 | tag1|
---- -----
Also, you need to add message identifier field into the tbl-used to get message, linked with every row.
Another variant (not preferable):
You need tbl-tags only if you want to use similar tags in many messages (after receving message, you can normalize case, split it and append only new tags into the tbl-tags), but if you don't need this type of optimization, you can use "array field" in the message table (i.e., in mysql you can do this in the similar way: How can I simulate an array variable in MySQL?).

Related

MySql get information from table via a foreign key to display in new table [duplicate]

Let's say I have two tables, Faculty and FacultyEmail who have a 1:1 relationship, with the following attributes:
+-------------+ +--------------+
| Faculty | | FacultyEmail |
+-------------+ +--------------+
| FacultyID | | FacultyID |
| FirstName | | Email |
| LastName | +--------------+
+-------------+
I need to auto generate the Email column based on the FirstName and LastName fields in Faculty such as: firstname.lastname#gmail.com
My question is whether this is possible? And if so, how would I go about writing up the DDL for this?
This is the DDL I have so far:
CREATE TABLE IF NOT EXISTS `Faculty`
(
`FacultyID` INT NOT NULL,
`FirstName` VARCHAR(45) NOT NULL,
`LastName` VARCHAR(45) NOT NULL,
PRIMARY KEY (`FacultyID`)
);
CREATE TABLE IF NOT EXISTS `FacultyEmail`
(
`FacultyID` INT NOT NULL,
`Email` VARCHAR(90) GENERATED ALWAYS AS (...), -- What goes here?
PRIMARY KEY (`FacultyID`)
FOREIGN KEY (`FacultyID`)
REFERENCES `Faculty` (`FacultyID`)
ON DELETE CASCADE
ON UPDATE CASCADE
);
Let's say I have two tables, Faculty and FacultyEmail who have a 1:1 relationship, with the following attributes:
If it's truly a 1:1 relationship, a second table is an unnecessary complication. With a single table a generated column can be used.
CREATE TABLE IF NOT EXISTS `Faculty`
(
FacultyID int not null primary key auto_increment,
FirstName varchar(255) not null,
LastName varchar(255) not null,
Email varchar(255) as
(lower(concat(firstname,'.',lastname,'#wossamotta.edu'))) stored not null,
unique(Email)
);
However, a generated column cannot be updated. Your Faculty may not appreciate being unable to pick their email address. Consider using default instead.
Email varchar(255) not null
default(lower(concat(firstname,'.',lastname,'#wossamotta.edu'))),
Unfortunately since it's a data specification to make the faculty have that specific email and table
If you must have a FacultyEmail "table" for compatibility, consider a view.
create view FacultyEmail
as select FacultyID, Email from Faculty
Try it.
If you really want two tables, you can write an after insert trigger.
create trigger default_faculty_email
after insert
on Faculty for each row
insert into FacultyEmail (facultyid, email)
values(
NEW.facultyid,
lower(CONCAT(NEW.firstname,'.',NEW.lastname,'#wossamotta.edu'))
);
However, after that there's nothing preventing a FacultyEmail from being deleted leaving a Faculty with no FacultyEmail. So wee need another trigger to prevent that.
create trigger ProtectFacultyEmail
before delete
on FacultyEmail for each row
signal sqlstate '45000'
set message text = 'FacultyEmail cannot be directly deleted. Delete the Faculty row instead.';
This prevents directly deleting from FacultyEmail, but allows the on delete cascade.
As you can see, a second table raises a lot of complications.
Try it.
Notes
I don't believe there is a need for an on update cascade, there's nothing in FacultyEmail to update. If the FacultyID can change that's a poor primary key. Consider an independent ID column separate from any mutable ID.
varchar(45) does not save any space, it only sets a maximum size. Maximum size is a business rule and should not be hard-coded into the schema.

USING STORED PROCEDURE to SUM, GROUP BY and insert to another table with

I have created 3 tables: item, shop and stock. Plus a stored procedure called inserting
which inserts to the shop table with a given item from the item table
CREATE TABLE item(
i_id int(11) auto_increment,
i_name varchar(255) not null,
primary key(i_id));
CREATE TABLE shop(
s_id int(11) auto_increment,
s_name varchar(255) not null,
s_item int(11) not null,
s_qty int(11) not null,
primary key(s_id),
foreign key(s_item) references item(i_id)
);
CREATE TABLE stock(
item int(11) not null,
total int(11) not null
);
CREATE PROCEDURE inserting (
IN shop_name varchar(225),
IN shop_item int(11),
IN shop_qty int(11)
)
BEGIN
INSERT INTO shop(s_name, s_item, s_qty)
VALUES
(shop_name, shop_item, shop_qty);
INSERT INTO STOCK(item, total)
SELECT s_item, SUM(s_qty) FROM shop GROUP BY s_item
ON DUPLICATE KEY UPDATE
item = VALUES(item),
total = VALUES(total);
The first insert works, but on the second insert when it populates the stock table it gives me extra columns, which i'm not expecting.
I have tried using REPLACE INTO and ON DUPLICATE KEY UPDATE to get single results, still the results comes as the following:
SELECT * FROM `stock`;
+------+-------+
| ITEM | TOTAL |
+------+-------+
| 1 | 5 |
| 1 | 9 |
+------+-------+
what I am trying to achieve is, group the ITEM column, and sum up the TOTAL to a single row.
what am I doing wrong here, or missing from the query?
thanks.
For the on duplicate key syntax to work as expected, you need a unique or primary key constraint on the target table, so the database can identify the "duplicate" rows. Same goes for the REPLACE syntax.
But your stock table does not have a primary key. Consider the following DDL instead:
CREATE TABLE stock(
item int(11) primary key,
total int(11) not null
);
Side note: there is no need to reassign column item in the on duplicate key clause, since it's what is used to identify the conflict in the first place. This is good enough:
INSERT INTO STOCK(item, total)
SELECT s_item, SUM(s_qty) FROM shop GROUP BY s_item
ON DUPLICATE KEY UPDATE total = VALUES(total);
If you run this one time, it should work as you expected. But subsequent runs may bring duplicate ITEM because of what #gmb said. The table must have a UNIQUE index or PRIMARY KEY. See more details here
https://dev.mysql.com/doc/refman/5.7/en/insert-on-duplicate.html

Insert auto-incremental id on Kettle/Spoon

I have a single column of "names" on a text file.
And I want to add this column to my database's table "names" that already exists and has a lot of names.
That's looked very simple, but I don't know how to add the auto-incremental ID
I have something like this:
names
John
Lars
Peter
I wanted something like this.
id | names
.........
68 | John
69 | Lars
70 | Peter
This is how I create my table:
CREATE TABLE IF NOT EXISTS `names` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '',
`name` VARCHAR(45) NOT NULL COMMENT '',
PRIMARY KEY (`id`) COMMENT '')
ENGINE = InnoDB;
There are two details to take in consideration:
1 - If you do not want two rows with the same name in the database.
To accomplish that you must set only the name field in the lookup part of the insert/update task.
2 - If you can have two rows with the same name.
Do not put anything in the lookup part of the insert/update task.
Kettle will not include the ID colummn in insert on both cases. Mysql will define the next ID automatically as the ID field is marked as auto_icrement.
UPDATE
Please, take a look in the target table field. You have defined the "domain" table instead of "names".

Need help designing my invoice db structure

I have a website where customer can buy subscriptions.
The customer can at any time go to payment history and see what has ben bought.
I'm trying to design the db for creating invoices, but something doesn't seem right for me.
My current setup looks like this:
+-----------+--------------+---------+
| Invoice | invoice_item | product |
+-----------+--------------+---------+
| id | id | id |
| fk_userID | desc | name |
| | quantity | price |
| | sum | |
| | fk_invoiceID | |
+-----------+--------------+---------+
It seems logical that invoice_item has a foreign key referenced to product.
But what happens if a product is deleted? If they are related, then row in the item_list will be deleted or set to null.
And that will not work if you want to look at an old invoice and the product is no longer available.
So, should Product and Item_list be related?
You cannot remove a product once it has been defined, so add a Status field to the product that - in this example I'm using an enum, although it could easily be an INT or a set of bools (i.e. Archived), I use Parameter Enumeration Tables for this but that's a seperate answer.
The most important thing is to ensure that the invoice line has the pricing (and description) taken from the product at the point of order, to ensure that any future pricing changes or product name changes don't affect pre-existing invoices.
The other technique that I have used (quite succesfully) is to introduce the concept of superceding entities in a database - so that the original record remains and a new version is inserted whenever data is changed. To do this I add the following fields :
currentID
supersededById
previousId
It makes the queries a little more cumbersome - but especially for addresses it is essential to ensure that the invoices remain constant and that address changes aren't reflected in the invoices - e.g. changing company name shouldn't change previously raised invoices.
CREATE TABLE `Invoice` (
`id` INTEGER NOT NULL AUTO_INCREMENT ,
PRIMARY KEY (`id`)
);
CREATE TABLE `Invoice Item` (
`id` INTEGER NOT NULL AUTO_INCREMENT ,
`desc` VARCHAR(200) NOT NULL ,
`value` DECIMAL(11,3) NOT NULL ,
`quantity` DECIMAL(11,3) NOT NULL ,
`total` DECIMAL(11,3) NOT NULL ,
`fk_id_Invoice` INTEGER NOT NULL ,
`fk_id_Product` INTEGER NOT NULL ,
PRIMARY KEY (`id`)
);
CREATE TABLE `Product` (
`id` INTEGER NOT NULL AUTO_INCREMENT ,
`Price` DECIMAL(11,3) NOT NULL ,
`Name` VARCHAR(200) NOT NULL ,
`Status` ENUM NOT NULL ,
PRIMARY KEY (`id`)
);
ALTER TABLE `Invoice Item` ADD FOREIGN KEY (fk_id_Invoice) REFERENCES `Invoice` (`id`);
ALTER TABLE `Invoice Item` ADD FOREIGN KEY (fk_id_Product) REFERENCES `Product` (`id`);
You just need an additional column as no_of_stock, in the product table. If the product is empty in the inventory or is not used currently, should not be deleted, instead that column's value will be set to 0. Which will mean that though product cann ot be sold currently, but was previously sold, having the past existense
If you want people to be able to see their old invoices, no matter how long ago they were generated, and including all information, then you shouldn't delete products. Just add a column containing an ENUM('active', 'inactive') NOT NULL or similar to distinguish those products you currently offer from those you only keep around for old references.

Problem with an querying an array. (MySQL/PHP)

I have an array of strings inputted by the user from an dynamic form. I want to store each value of the array into a table along with an itemid (which is the same for all)
My query is currently inserting the whole array into one row's text_value with implode.
Is there a way instead of looping through the array and running a query for each value in the array, for me to query each array value with the itemId.
I was thinking perhaps adding another dimension to the array with the itemId? is this possible?
current query:
$query = "INSERT INTO answers_tb (item_id, text_value)VALUES('$itemid','".implode(',', $answers) . "')";
here is print_r of array:
Array ( [0] => option 1 [1] => option 2 [2] => option 3 [3] => option 4 )
here is the table structure I am inserting to (item_id is a foreign key):
**Field** | **Type** **Attributes**
answer_id | int(11) PRIMARY KEY
item_id | int(11) FOREIGN KEY
text_value | varchar(50)
the referenced table:
**Field** | **Type** | **Attributes**
item_id | int(11) | PRIMARY KEY
item_type | tinyint(1) |
user_id | int(11) |
unit_id | int(11) |
question_text | varchar(100)
question_text_2 | varchar(100)
item_desc | varchar(25)
item_name | varchar(25)
thanks
If you structure your table as item_id, astring rather than item_id, alongconcatenatedstring, you could do the insert like this:
$id=2;
$valueclause=function($string) use ($id) { return "('$id','$string')";};
array_walk($valueclause, $arr);
$values=implode(',',$arr);
$query= "INSERT INTO answers_tb (item_id, text_value) VALUES $values";
ETA: It appears that it might be useful to have a primary key that combines an auto_increment and another column. So given your table struture of:
**Field** | **Type**
answer_id | int(11)
item_id | int(11)
text_value | varchar(50)
you might consider indexing like this:
CREATE TABLE answers_tb(
item_id INT NOT NULL,
answer_id INT NOT NULL AUTO_INCREMENT,
text_value CHAR(50) NOT NULL,
PRIMARY KEY (item_id, answer_id)//note the 2 columns in the key
);
Then when you insert like this:
INSERT INTO answers_tb (item_id, text_value)
VALUES (1,'thing'), (1,'foo'),
(17,'blah'),
(6,'beebel'), (6,'bar');
your resulting data will look like this:
item_id, answer_id, textvalue
1, 1, thing
1, 2, foo
17, 1, blah
6, 1, beebel
6, 2, bar
It sounds like you would be better served with a different table design.
Instead of answers_tb (item_id, text_value), use answers_tb (item_id, offset, value).
(The primary key would be (item_id, offset).)
Then you would find it much easier to query the table.
EDIT: You posted the following table structure:
**Field** | **Type** **Attributes**
answer_id | int(11) PRIMARY KEY
item_id | int(11) FOREIGN KEY
text_value | varchar(50)
If I understand the table design right, your design works like this:
Each row of the referenced table (let's call it questions) represents a single question asked by a user of your application. It has a question ID, and the ID of the user who posted it.
Each row of the table answers_tb represents the set of all answers to the question in the row of table questions referenced by item_id. Answers are distinguished by the order in which they appear in the column entry.
What I'm saying is that this design for answers_tb doesn't work very well, for the reason you've identified: it's difficult to query against the answers stored in the "array" column. That is why this design is problematic. A better design would be as follows:
**Field** | **Type**
item_id | int(11)
answer_number | int
text_value | varchar(50)
wherein item_id is still a foreign key, but the primary key is (item_id, answer_number). In this design, each row of the table, rather than containing the set of all answers to the corresponding question, would contain just one answer to that question. The rows are distinguished from one another by the different values in answer_number, but you know which question each row corresponds to by the value in item_id. This design is much easier to query against.
It is a general rule that you ought not to try to store an array of data in a column, because it makes it problematic to search against. In some cases it makes sense to break that rule, but you have to be able to recognise when you are in such a case. In this case, you want to search by the stored values, so you should not do it.