What mysql table structure to use in this case - php

I need to make the databse structure for a real estate website where users can create properties of many types and with many features related to the property.
The main categories will be:
1. House (subtype apartment, house, loft)
2. Commercial (subtype: hotels, buildings, offices, factory)
3. Terrains (subtype: urban, agricola, industrial, for sports)
All this above can have many features defined, for example an apartment: light, gas, # of rooms, bathrooms, floor number, balcony, and so on, and this features are diferent from one property type to another.
At the moment I have one master table named property containing the basic info like address and price, and three subtables property_house, property_commercial, and property_terrain with as many fields as features a property can have.
Is this structure okay? I need to do the creation and modification of all the property types into one form maybe with 3-4 steps and will differ from one property type to another. Will it be easier if I have just one master table like property and a second property_features where to store the property_id, feature_name, and feature_value? What's best for performance and maintaining? What would you people vote for?
Thank you! :)

I have experience with both ways you have mentioned. ( I'm co-developer of iRealty http://www.irealtysoft.com/ ver 3. and ver 4 have two different storage methods). After several years of dealing with both ways I recommend to create a single table for all properties. This pattern is called Single Table Inheritance (http://martinfowler.com/eaaCatalog/singleTableInheritance.html by Martin Fowler).
I see only two disadvantages of this method:
field names should be unique within all property types
a lot of records will have NULL in about a have of their columns which wastes disk space a little bit
A the same time with this database structure all CRUD routines are very simple and straightforward. You will save a lot of time building queries/ORM layer. With this structure you are free to create indexes and utilize arithmetic and other database functions in WHERE clauses and avoid costly JOINs.
The disk space is cheap, the development time is expensive.
The | property_id | feature_name | feature_value | allows to keep the same database structure when changing fields and property types, which is good when you have a complex upgrade/update routines. If you are going to build a single (production) instance application the upgrades should not be an issue. However this method make CRUD model complex and hence more expensive and bug-prone. (More code --- more bugs.)

Well, are these three main categories set in stone? Is there a possibility of a fourth one cropping up in the future? I would probably go with something like this:
CREATE TABLE property (
id int not null auto_increment,
name varchar(250) not null,
property_type int not null,
property_subtype int not null,
primary key(id)
);
CREATE TABLE property_type (
id int not null auto_increment,
name varchar(250) not null,
primary key(id)
);
CREATE TABLE property_subtype (
id int not null auto_increment,
type int not null,
name varchar(250) not null,
primary key(id)
);
CREATE TABLE property_feature (
id int not null auto_increment,
property int not null,
feature int not null,
value varchar(250) not null,
primary key(id)
);
CREATE TABLE property_feature (
id int not null auto_increment,
feature int not null,
value varchar(250) not null,
primary key(id)
);
I think this would be the most effective in the long run and the most flexible if - when - the time comes.
With this structure, you can then add the data like this:
mysql> INSERT INTO property_type (name) VALUES ('House'),('Commercial'),('Terrains');
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> INSERT INTO property_subtype (type, name) VALUES (1, 'Apartment'),(1, 'House'), (1,'Loft');
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> INSERT INTO subtype_feature (subtype, name) VALUES (1, 'Light'),(1, 'Floor #');
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> INSERT INTO property (name, property_type, property_subtype) VALUES ('Som
e Apartment', 1, 1);
Query OK, 1 row affected (0.01 sec)
mysql> INSERT INTO property_feature (feature, value) VALUES (1, 'Yes'),(2, '5th');
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> INSERT INTO property_feature (property, feature, value) VALUES (1, 1, 'Yes'),(1, 2, '5th');
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
You can then get all the features of a particular property pretty easily:
mysql> SELECT s.name, f.value FROM property_feature f INNER JOIN subtype_feature
s ON f.feature = s.id WHERE f.property = 1;
+---------+-------+
| name | value |
+---------+-------+
| Light | Yes |
| Floor # | 5th |
+---------+-------+
2 rows in set (0.00 sec)

Related

Arrays in Mysql database for image path storing? [duplicate]

I have two tables in MySQL. Table Person has the following columns:
id
name
fruits
The fruits column may hold null or an array of strings like ('apple', 'orange', 'banana'), or ('strawberry'), etc. The second table is Table Fruit and has the following three columns:
fruit_name
color
price
apple
red
2
orange
orange
3
-----------
--------
------
So how should I design the fruits column in the first table so that it can hold array of strings that take values from the fruit_name column in the second table? Since there is no array data type in MySQL, how should I do it?
The proper way to do this is to use multiple tables and JOIN them in your queries.
For example:
CREATE TABLE person (
`id` INT NOT NULL PRIMARY KEY,
`name` VARCHAR(50)
);
CREATE TABLE fruits (
`fruit_name` VARCHAR(20) NOT NULL PRIMARY KEY,
`color` VARCHAR(20),
`price` INT
);
CREATE TABLE person_fruit (
`person_id` INT NOT NULL,
`fruit_name` VARCHAR(20) NOT NULL,
PRIMARY KEY(`person_id`, `fruit_name`)
);
The person_fruit table contains one row for each fruit a person is associated with and effectively links the person and fruits tables together, I.E.
1 | "banana"
1 | "apple"
1 | "orange"
2 | "straberry"
2 | "banana"
2 | "apple"
When you want to retrieve a person and all of their fruit you can do something like this:
SELECT p.*, f.*
FROM person p
INNER JOIN person_fruit pf
ON pf.person_id = p.id
INNER JOIN fruits f
ON f.fruit_name = pf.fruit_name
The reason that there are no arrays in SQL, is because most people don't really need it. Relational databases (SQL is exactly that) work using relations, and most of the time, it is best if you assign one row of a table to each "bit of information". For example, where you may think "I'd like a list of stuff here", instead make a new table, linking the row in one table with the row in another table.[1] That way, you can represent M:N relationships. Another advantage is that those links will not clutter the row containing the linked item. And the database can index those rows. Arrays typically aren't indexed.
If you don't need relational databases, you can use e.g. a key-value store.
Read about database normalization, please. The golden rule is "[Every] non-key [attribute] must provide a fact about the key, the whole key, and nothing but the key.". An array does too much. It has multiple facts and it stores the order (which is not related to the relation itself). And the performance is poor (see above).
Imagine that you have a person table and you have a table with phone calls by people. Now you could make each person row have a list of his phone calls. But every person has many other relationships to many other things. Does that mean my person table should contain an array for every single thing he is connected to? No, that is not an attribute of the person itself.
[1]: It is okay if the linking table only has two columns (the primary keys from each table)! If the relationship itself has additional attributes though, they should be represented in this table as columns.
MySQL 5.7 now provides a JSON data type. This new datatype provides a convenient new way to store complex data: lists, dictionaries, etc.
That said, arrays don't map well databases which is why object-relational maps can be quite complex. Historically people have stored lists/arrays in MySQL by creating a table that describes them and adding each value as its own record. The table may have only 2 or 3 columns, or it may contain many more. How you store this type of data really depends on characteristics of the data.
For example, does the list contain a static or dynamic number of entries? Will the list stay small, or is it expected to grow to millions of records? Will there be lots of reads on this table? Lots of writes? Lots of updates? These are all factors that need to be considered when deciding how to store collections of data.
Also, Key/Value data stores, Document stores such as Cassandra, MongoDB, Redis etc provide a good solution as well. Just be aware of where the data is actually being stored (if its being stored on disk or in memory). Not all of your data needs to be in the same database. Some data does not map well to a relational database and you may have reasons for storing it elsewhere, or you may want to use an in-memory key:value database as a hot-cache for data stored on disk somewhere or as an ephemeral storage for things like sessions.
A sidenote to consider, you can store arrays in Postgres.
In MySQL, use the JSON type.
Contra the answers above, the SQL standard has included array types for almost twenty years; they are useful, even if MySQL has not implemented them.
In your example, however, you'll likely want to create three tables: person and fruit, then person_fruit to join them.
DROP TABLE IF EXISTS person_fruit;
DROP TABLE IF EXISTS person;
DROP TABLE IF EXISTS fruit;
CREATE TABLE person (
person_id INT NOT NULL AUTO_INCREMENT,
person_name VARCHAR(1000) NOT NULL,
PRIMARY KEY (person_id)
);
CREATE TABLE fruit (
fruit_id INT NOT NULL AUTO_INCREMENT,
fruit_name VARCHAR(1000) NOT NULL,
fruit_color VARCHAR(1000) NOT NULL,
fruit_price INT NOT NULL,
PRIMARY KEY (fruit_id)
);
CREATE TABLE person_fruit (
pf_id INT NOT NULL AUTO_INCREMENT,
pf_person INT NOT NULL,
pf_fruit INT NOT NULL,
PRIMARY KEY (pf_id),
FOREIGN KEY (pf_person) REFERENCES person (person_id),
FOREIGN KEY (pf_fruit) REFERENCES fruit (fruit_id)
);
INSERT INTO person (person_name)
VALUES
('John'),
('Mary'),
('John'); -- again
INSERT INTO fruit (fruit_name, fruit_color, fruit_price)
VALUES
('apple', 'red', 1),
('orange', 'orange', 2),
('pineapple', 'yellow', 3);
INSERT INTO person_fruit (pf_person, pf_fruit)
VALUES
(1, 1),
(1, 2),
(2, 2),
(2, 3),
(3, 1),
(3, 2),
(3, 3);
If you wish to associate the person with an array of fruits, you can do so with a view:
DROP VIEW IF EXISTS person_fruit_summary;
CREATE VIEW person_fruit_summary AS
SELECT
person_id AS pfs_person_id,
max(person_name) AS pfs_person_name,
cast(concat('[', group_concat(json_quote(fruit_name) ORDER BY fruit_name SEPARATOR ','), ']') as json) AS pfs_fruit_name_array
FROM
person
INNER JOIN person_fruit
ON person.person_id = person_fruit.pf_person
INNER JOIN fruit
ON person_fruit.pf_fruit = fruit.fruit_id
GROUP BY
person_id;
The view shows the following data:
+---------------+-----------------+----------------------------------+
| pfs_person_id | pfs_person_name | pfs_fruit_name_array |
+---------------+-----------------+----------------------------------+
| 1 | John | ["apple", "orange"] |
| 2 | Mary | ["orange", "pineapple"] |
| 3 | John | ["apple", "orange", "pineapple"] |
+---------------+-----------------+----------------------------------+
In 5.7.22, you'll want to use JSON_ARRAYAGG, rather than hack the array together from a string.
Use database field type BLOB to store arrays.
Ref: http://us.php.net/manual/en/function.serialize.php
Return Values
Returns a string containing a byte-stream representation of value that
can be stored anywhere.
Note that this is a binary string which may include null bytes, and
needs to be stored and handled as such. For example, serialize()
output should generally be stored in a BLOB field in a database,
rather than a CHAR or TEXT field.
you can store your array using group_Concat like that
INSERT into Table1 (fruits) (SELECT GROUP_CONCAT(fruit_name) from table2)
WHERE ..... //your clause here
HERE an example in fiddle

Is there a way to show a WHERE clause just for one field in MySQL?

I do have some courses, to which students can enroll.
There's a users table (usuarios), a courses (cursos) table and a third table that shows each student enrollment (cursosUsuarios), like this:
CREATE TABLE usuarios(
userID int unsigned not null auto_increment primary key,
userName char(50) null,
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE cursos (
cursoID int unsigned not null auto_increment primary key,
nombreCurso char(100) not null,
estadoCurso char(50) not null,
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE cursosUsuarios (
cursosUsuariosID int unsigned not null auto_increment primary key,
userID int not null,
cursoID int not null,
userHabilitado int(1) not null DEFAULT '0',
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
Each user can be enrolled in each course, but up until it is "enabled" by the admin, it cannot access the class. Being enabled means that the "userHabilitado" field is "1" instead of the default "0".
Now, I'm trying to show a list of all available courses at the user's profile, so the admin can see in which ones the user has enrolled and change that "0" to "1" in the "userHabilitado" field.
This is my query:
SELECT
cursos.cursoID AS idcurso, cursos.estadoCurso,
cursosUsuarios.userHabilitado AS 'ok',
GROUP_CONCAT(cursosUsuarios.userID SEPARATOR ',') AS 'usuarios'
FROM cursos LEFT JOIN cursosUsuarios
ON cursos.cursoID = cursosUsuarios.cursoID
LEFT JOIN usuarios
ON cursosUsuarios.userID = usuarios.userID
WHERE cursos.estadoCurso='abierto'
GROUP BY cursos.cursoID;
This is the result I've got:
Now, as I need to show all the open courses (the courses where the estadoCurso field is set to "abierto"), I've not added a WHERE clause to filter the results by user.
What I do is to take the user ID from the url and explode the "usuarios" field to find the user that I'm looking for, like this:
echo '<div class="checkbox">';
while ($curso=mysqli_fetch_assoc($buscarCurso)) {
$usuario = $_GET['userID'];
$usuarios = explode(',', $curso['usuarios']);
?>
<input type="checkbox"
<?php if (in_array($usuario, $usuarios)) {echo 'checked';}?>
name="cursoID[]" value="<?php echo $curso['idcurso'];?>">
<?php echo '<b>'.$curso['nombreCurso'].'</b> ';?>
<?php echo 'Id. curso: '.$curso['idcurso'].' <b>Estado usuario:</b> <input type="text" name="ok['.$curso['idcurso'].']" value ="'.$curso['ok'].'">';?>
So, if the user ID is in the $usuarios array the checkbox is checked, as it means that the user is enrolled.
My problem is how to show the userHabilitado field in that list.
In the image above, the user 70 is enrolled in three courses: courseID 15, 16 and 18. In each case the userHabilitado field is set to "1", but the result is incorrect there.
I want to replace cursosUsuarios.userHabilitado AS 'ok' to something like:
cursosUsuarios.userHabilitado WHERE usuarios.userID = 70, but inside that field.
Is that possible within MySQL?
Prior to MySQL 5.7 the default was to allow non FULL group by. Which means you can have a group by (that uses aggregate functions like sum and max and count and group_concat) with other non-aggregated columns (let's call them NON AGGS) like your first 3 shown not all part of your group by clause. It allowed it but the results would typically work out like this:
It worked great because you know your data well and are trying to achieve a distinct
It worked out awful because it was a snafu
Prior to 5.7, ONLY_FULL_GROUP_BY existed but it was turned OFF by default.
So in MySQL 5.7 along comes the ONLY_FULL_GROUP_BY defaulted ON. As such if you attempt a group by, but with not all the NON AGGS in the group by clause, you would get an Error.
Consider the following problem in 5.6 below:
create table thing
( col1 int not null,
col2 int not null,
age int not null
);
insert thing(col1,col2,age) values
(1,2,10),
(1,3,20),
(2,3,20),
(2,2,10);
select col1,col2,max(age) from thing group by col1;
+------+------+----------+
| col1 | col2 | max(age) |
+------+------+----------+
| 1 | 2 | 20 |
| 2 | 3 | 20 |
+------+------+----------+
What happens above is not all the NON AGGS are in the group by. It returns the max(age) by col1. But since col2 was not in the group by, it used the Cluster Index or Physical Ordering and brought it, inadvertently perhaps (a snafu, a mistake), the wrong value for col2. Depending on your intentions or knowing your data or even caring. The engine didn't care; perhaps you do.
To avoid these common mistakes or inadvertent data return, MySQL 5.7 turns on ONLY_FULL_GROUP_BY by default.
In your case, the wrong rows are making up your results presumably for columns 2 and 3.
See the Manual Page entitled MySQL Handling of GROUP BY.
Example 2
-- drop table if exists person;
create table person
( id int auto_increment primary key,
firstName varchar(100) not null,
lastName varchar(100) not null
);
-- drop table if exists fruitConsumed;
create table fruitConsumed
( id int auto_increment primary key,
theDate date not null,
fruitId int not null, -- does not really matter. Say, 1=apple, 2=orange from some other table
personId int not null,
qty int not null
);
-- truncate table person;
insert person (firstName,lastName) values
('Dirk','Peters'),
('Dirk','Smith'),
('Jane','Billings');
-- truncate table fruitConsumed;
insert fruitConsumed (theDate,fruitId,personId,qty) values
('2016-10-31',1,1,2),
('2016-10-31',2,1,5),
('2016-10-31',2,2,12),
('2016-11-02',2,2,3);
Query:
select p.firstName,p.lastName,sum(fc.qty)
from person p
join fruitConsumed fc
on fc.personId=p.id
group by p.firstName,p.lastName;
+-----------+----------+-------------+
| firstName | lastName | sum(fc.qty) |
+-----------+----------+-------------+
| Dirk | Peters | 7 |
| Dirk | Smith | 15 |
+-----------+----------+-------------+
The above works great on MySQL 5.6 and 5.7 regardless of the setting for ONLY_FULL_GROUP_BY
now consider
select p.firstName,p.lastName,sum(fc.qty)
from person p
join fruitConsumed fc
on fc.personId=p.id
group by p.firstName;
+-----------+----------+-------------+
| firstName | lastName | sum(fc.qty) |
+-----------+----------+-------------+
| Dirk | Peters | 22 |
+-----------+----------+-------------+
The above is often acceptable on MySQL 5.6 without ONLY_FULL_GROUP_BY enabled and fails on 5.7 with ONLY_FULL_GROUP_BY enabled (error 1055). The above output is basically gibberish. But below it is explained somewhat:
We know that Dirk, a Dirk, just one Dirk, is the only one to survive the inner join. There are 2 Dirks. But because of the group by p.firstName, we are left with just one Dirk. We need a lastName. Because of the non-conformity to
the SQL Standard, MySQL can allow this with ONLY_FULL_GROUP_BY turned off. So it just picks any old lastName. Well, the first one it finds, and that is either in cache or the one in the physical ordering.
And it went with Peters. The fruit count sum is for all Dirks.
So if you code like this, the non-conforming non-ONLY_FULL_GROUP_BY gives you gibberish.
And as stated, MySQL 5.7 ships defaulted to not allowing this. But it is tweakable to the old way if you choose.
It is highly recommended that you fix your queries and leave ONLY_FULL_GROUP_BY as enabled.
Your query is against the sql standard because you have fields in the select list that are neither in the group by clause, nor are subject of an aggregate function.
Specifically, you have cursosUsuarios.userHabilitado AS 'ok' fieldin the select list, but you only group by on the cursos.cursoID field. This means, that if you have enrolled an not enrolled students in a course, all of them will be shown as enrolled or not enrolled. Extend your group by clause to comply with the sql standard and to get the expected results:
...GROUP BY cursos.cursoID, cursos.estadoCurso, cursosUsuarios.userHabilitado;

Hierarchical Query in MySQL II

I'm trying to figure out a way to display the number of grandchildren, great grandchildren, etc. on a website focusing on animals. Someone told me about a really cool query # Hierarchical queries in MySQL
Below is my adaptation.
$stm = $pdo->prepare("SELECT COUNT(#id := (
SELECT `Taxon`
FROM gz_life_mammals
WHERE `Parent` = #id
)) AS numDescendants
FROM (
SELECT #id := :MyURL
) vars
STRAIGHT_JOIN gz_life_mammals
WHERE #id IS NOT NULL");
$stm->execute(array(
'MyURL'=>$MyURL
));
while ($row = $stm->fetch())
{
$ChildrenCount = $row['numDescendants'];
}
echo $ChildrenCount;
I think I have it set up to count children, actually, but I'll work on grandchildren next. Anyway, when I navigate to a species page, it correctly displays a count of 0. But when I navigate to a parent page, I get this error message:
Cardinality violation: 1242 Subquery returns more than 1 row
Can anyone tell me what's going on and how I can fix that?
My database table features animal taxa in a parent-child relationship in the field Taxon, like this:
Taxon | Parent
Mammalia | Chordata
Carnivora | Mammalia
Canidae | Carnivora
Canis | Canidae
Canis-lupus | Canis
To see information about the wolf (Canis lupus), I would navigate to MySite/life/canis-lupus
ON EDIT
Here's the table schema. I can't make it work with SQFiddle, though; one error after another.
CREATE TABLE t (
N INT(6) default None auto_increment,
Taxon varchar(50) default NULL,
Parent varchar(25) default NULL,
NameCommon varchar(50) default NULL,
Rank smallint(2) default 0
PRIMARY KEY (N)
) ENGINE=MyISAM
Hopefully one would agree that this is not an answer-only Answer without explanation, since the code is quite documented throughout.
Basically, it is a self-join table with a row having a reference to who its parent is. The stored proc will use a worktable to find children, children-of-children, etc. And maintain a level.
For instance, level=1 represents children, level=2 represents grandchildren, etc.
At the end, the counts are retrieved. As the id's are in the worktable, expand as you wish with it.
Schema
create schema TaxonSandbox; -- create a separate database so it does not mess up your stuff
use TaxonSandbox; -- use that db just created above (stored proc created in it)
-- drop table t;
CREATE TABLE t (
N int auto_increment primary key,
Taxon varchar(50) not null,
Parent int not null, -- 0 can mean top-most for that branch, or NULL if made nullable
NameCommon varchar(50) not null,
Rank int not null,
key(parent)
);
-- truncate table t;
insert t(taxon,parent,NameCommon,rank) values ('FrogGrandpa',0,'',0); -- N=1
insert t(taxon,parent,NameCommon,rank) values ('FrogDad',1,'',0); -- N=2 (my parent is N=1)
insert t(taxon,parent,NameCommon,rank) values ('FrogMe',2,'',0); -- N=3 (my parent is N=2)
insert t(taxon,parent,NameCommon,rank) values ('t4',1,'',0); -- N=4 (my parent is N=2)
insert t(taxon,parent,NameCommon,rank) values
('t5',4,'',0),('t6',4,'',0),('t7',5,'',0),('t8',5,'',0),('t9',7,'',0),('t10',7,'',0),('t11',7,'',0),('t12',11,'',0);
Stored Procedure
use TaxonSandbox;
drop procedure if exists showHierarchyUnder;
DELIMITER $$ -- will be discussed separately at bottom of answer
create procedure showHierarchyUnder
(
theId int -- the id of the Taxon to search for it's decendants (my awkward verbiage)
)
BEGIN
-- theId parameter means i am anywhere in hierarchy of Taxon
-- and i want all decendent Taxons
declare bDoneYet boolean default false;
declare working_on int;
declare next_level int; -- parent's level value + 1
declare theCount int;
CREATE temporary TABLE xxFindChildenxx
( -- A Helper table to mimic a recursive-like fetch
N int not null, -- from OP's table called 't'
processed int not null, -- 0 for not processed, 1 for processed
level int not null, -- 0 is the id passed in, -1=trying to figure out, 1=children, 2=grandchildren, etc
parent int not null -- helps clue us in to figure out level
-- NOTE: we don't care about level or parent when N=parameter theId passed into stored proc
-- in fact we will be deleting that row near the bottom or proc
);
set bDoneYet=false;
insert into xxFindChildenxx (N,processed,level,parent) select theId,0,0,0; -- prime the pump, get sp parameter in here
-- stay inside below while til all retrieved children/children of children are retrieved
while (!bDoneYet) do
-- see if there are any more to process for children
-- simply look in worktable for ones where processed=0;
select count(*) into theCount from xxFindChildenxx where processed=0;
if (theCount=0) then
-- found em all, we are done inside this while loop
set bDoneYet=true;
else
-- one not processed yet, insert its children for processing
SELECT N,level+1 INTO working_on,next_level FROM xxFindChildenxx where processed=0 limit 1; -- order does not matter, just get one
-- insert the rows where the parent=the one we are processing (working_on)
insert into xxFindChildenxx (N,processed,level,parent)
select N,0,next_level,parent
from t
where parent=working_on;
-- mark the one we "processed for children" as processed
-- so we processed a row, but its children rows are yet to be processed
update xxFindChildenxx set processed=1 where N=working_on;
end if;
end while;
delete from xxFindChildenxx where N=theId; -- don't really need the top level row now (stored proc parameter value)
select level,count(*) as lvlCount from xxFindChildenxx group by level;
drop table xxFindChildenxx;
END
$$ -- tell mysql that it has reached the end of my block (this is important)
DELIMTER ; -- sets the default delimiter back to a semi-colon
Test Stored Proc
use TaxonSandbox; -- create a separate database so it does not mess up your stuff
call showHierarchyUnder(1);
+-------+----------+
| level | lvlCount |
+-------+----------+
| 1 | 2 |
| 2 | 3 |
| 3 | 2 |
| 4 | 3 |
| 5 | 1 |
+-------+----------+
So there are 2 children, 3 grandchildren, 2 great-grandchildren, 3 great-great, and 1 great-great-great
Were one to pass an id to the stored proc that does not exist, or one that has no children, no result set rows are returned.
Edit:
other comments, due to leaving the OP hanging on understanding his first stored proc creation I believe. Plus other questions that point back here.
Delimiters
Delimiters are important to wrap the block of the stored proc creation. The reason is so that mysql understands that the sequence of statements that follow are still part of the stored proc until it reaches the specified delimiter. In the case above, I made up one called $$ that is different from the default delimiter of a semi-colon that we are all used to. This way, when a semi-colon is encountered inside the stored proc during creation, the db engine will just consider it as one the many statements inside of it instead of terminating the stored proc creation. Without doing this delimiter wrapping, one can waste hours trying to create their first stored proc getting Error 1064 Syntax errors. At the end of the create block I merely have a line
$$
which tell mysql that that is the end of my creation block, and then the default delimiter of a semi-colon is set back with the call to
DELIMITER ;
Mysql manual page Using Delimiters with MySqlScript. Not a great manual page imo, but trust me on this one. Same issue when creating Triggers and Events.
PHP
To call this stored proc from php, it is just a string, "call showHierarchyUnder(1)". It returns a result set as described above, which, as described, can return a result set with no rows.
Remember that the 1 is a parameter to the stored proc. And that this exists in a database created, called TaxonSandbox if you followed the above.

Permissions: bitwise operations or many-to-many child table?

I am trying to understand the best way to work with permissions and as far as I'm aware there are two major options.
The first option is to use bitwise operations, and for this I would use the following database structure:
users
user_id | user_permission
---------------------
1 | 15
2 | 1
permissions
permission_id | permission_name
-----------------------
1 | Read
2 | Write
4 | Execute
8 | Delete
And then to check that the user has permission I would use the operation:
$user_permission & $permission_id
The main benefits I see to this are:
Trivial to set, get, and validate permissions
Less storage (no child database; no additional rows per user permission)
The main drawbacks I see to this are:
Listing users' permissions slightly more complicated
Cannot use foreign key constraints
Limited permissions (64 if using BIGINT)
The second option is to use a many-to-many child table, and for this I would use the following database structure:
users
user_id
-------
1
2
permissions
permission_id | permission_name
-----------------------
1 | Read
2 | Write
3 | Execute
4 | Delete
user_permissions
user_id | permission_id
-----------------------
1 | 1
1 | 2
1 | 3
1 | 4
2 | 1
And then to check that the user has permission I would use the operation (where $user_permission is an array of permission_ids):
in_array($permission_id, $user_permission);
The main benefits I see to this are:
Can use foreign key constraints
Trivial to list users' permissions
Allows for a far greater number of permissions
The main drawbacks I see to this are:
Greater storage (child database; additional rows per user permission)
Setting and getting permissions slightly more complicated
Question
Which of these would be the better option? I see benefits and drawbacks to each and am unsure which would be more suitable. Although I am aware that context probably plays a role; so in which situations would bitwise operations be better and in which would a many-to-many child table be better? Or is there a third option of which I'm unaware?
I'm currently more inclined to use a many-to-many table for the benefits of foreign key constraints and a greater number of permission possibilities, but I wonder if I'm missing something else; bitwise operation permissions seem to be quite prevalent so I'd assume there is a good reason for using them.
I Think bitwise operator are the best way to implement user permission.
Here I am showing how we can implement it with Mysql.
Below is a sample tables with some sample data:
Table 1 : Permission table to store permission name along with it bit like 1,2,4,8..etc (multiple of 2)
CREATE TABLE IF NOT EXISTS `permission` (
`bit` int(11) NOT NULL,
`name` varchar(50) NOT NULL,
PRIMARY KEY (`bit`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Insert some sample data into the table.
INSERT INTO `permission` (`bit`, `name`) VALUES
(1, 'User-Add'),
(2, 'User-Edit'),
(4, 'User-Delete'),
(8, 'User-View'),
(16, 'Blog-Add'),
(32, 'Blog-Edit'),
(64, 'Blog-Delete'),
(128, 'Blog-View');
Table 2: User table to store user id,name and role. Role will be calculated as sum of permissions.
Example :
If user 'Ketan' having permission of 'User-Add' (bit=1) and 'Blog-Delete' (bit-64) so role will be 65 (1+64).
If user 'Mehata' having permission of 'Blog-View' (bit=128) and 'User-Delete' (bit-4) so role will be 132 (128+4).
CREATE TABLE IF NOT EXISTS `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`role` int(11) NOT NULL,
`created_date` datetime NOT NULL
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Sample data-
INSERT INTO `user` (`id`, `name`, `role`, `created_date`)
VALUES (NULL, 'Ketan', '65', '2013-01-09 00:00:00'),
(NULL, 'Mehata', '132', '2013-01-09 00:00:00');
Loding permission of user
After login if we want to load user permission than we can query below to get the permissions:
SELECT permission.bit,permission.name
FROM user LEFT JOIN permission ON user.role & permission.bit
WHERE user.id = 1
Here user.role "&" permission.bit is a Bitwise operator which will give output as -
User-Add - 1
Blog-Delete - 64
If we want to check weather a particular user have user-edit permission or not-
SELECT * FROM `user`
WHERE role & (select bit from permission where name='user-edit')
Output = No rows.
You can see also : http://goo.gl/ATnj6j
I would not go with the bitwise operations solution. Unless you are really really cramped for space, breaking this out into its own table and mapping table won't cost that much disk. It would be easier for people who aren't you to understand, and you can more easily enforce FK relationships this way. Also, as you mentioned, the number of permissions can grow virtually without limit. Depending on how you index the table, queries like "show me all users with Read permission" seems like it would be quicker to execute and easier to understand (that's subjective, I realize).

sum `counts` column on duplicate key when doing an UPDATE in MySQL

I have a count table, and i would like to be able to sum the counts when renaming one of the compound key fields. This is difficult to explain, so let me show you some SQL:
Example table:
CREATE TABLE `test` (
`id` int(11) NOT NULL,
`type` int(11) NOT NULL,
`count` int(11) NOT NULL,
PRIMARY KEY (`id`,`type`)
);
Insert some data:
INSERT INTO test VALUES(1, 10, 1);
INSERT INTO test VALUES(2, 20, 3);
INSERT INTO test VALUES(2, 10, 3);
INSERT INTO test VALUES(2, 30, 3);
Query the data:
mysql> SELECT SUM(count) as count FROM test WHERE id = 2;
+-------+
| count |
+-------+
| 9 |
+-------+
mysql> SELECT SUM(count) as count FROM test WHERE type = 10;
+-------+
| count |
+-------+
| 4 |
+-------+
Works very well, is fast and flexible.
Now, I'd like to remap type 10 to type 20
UPDATE test SET type = 20 WHERE type = 10;
Duplicate entry '2-20' for key 'PRIMARY'
Using ON DUPLICATE KEY UPDATE here is invalid.
I figure it should be possible with a creative insert, but i'm not sure. Can anyone think of an approach ?
One approach is to "loosen" the PRIMARY KEY, meaning, change it from a PRIMARY (unique) KEY to a non-unique KEY. This will allow for duplicate values, and will allow your UPDATE statement to succeed, such that you will have two (or more) rows with matching (id,type).
(Note that this makes updating a single row problematic, so you would likely want to add a new column which can be unique. An AUTO_INCREMENT column would work nicely for that.)
If you don't want to do that, then the other approach would be to "combine" the counts for the type 10 and type 20 rows together (for each id) into a type 20 row, set the count for the type 10 rows to zero, and (optionally) remove the redundant type 10 rows in a separate statement.
The statement below "combines" the type 10 and type 20 counts by "mapping" the 10 value to a 20 in the first select.
-- combine count for types 10 and 20 as new count for type 20
-- and set count to zero for type 10
INSERT INTO test (id, `type`, `count`)
SELECT t.id
, IF(t.`type`=10,20,t.`type`) AS new_type
, SUM(t.`count`) AS `count`
FROM test t
WHERE t.`type` IN (10,20)
GROUP BY id, new_type
UNION ALL
SELECT u.id
, u.`type`
, 0 AS `count`
FROM test u
WHERE u.`type` = 10
ON DUPLICATE KEY UPDATE `count` = VALUES(`count`);
-- remove unnecessary type 10 rows.
DELETE FROM test WHERE `type` = 10 AND `count` = 0;
Note: The IF() expression in the first SELECT is equivalent to:
CASE WHEN t.type = 10 THEN 20 ELSE t.type END
The second select is getting us all the type 10 rows that will need to be updated. We concatenate those two result sets using the UNION ALL operator.
Then we take the combined (concatenated) result set, and run them into an insert. Any place we hit a "duplicate" key, we perform the update action via the ON DUPLICATE KEY clause.
You will likely want to do this in a transaction (if you are using InnoDB) and verify that the first statement completes successfully before executing a DELETE. If the first statement throws an exception (maybe there is a wonky trigger), then you would want to ROLLBACK the transaction. You'd also want to ROLLBACK if the DELETE fails for some reason (perhaps there is a foreign key constraint that would be violated.)
Alternatively, you do not necessarily need to perform the DELETE, you could just leave the type 10 rows with counts of zero.
IMPORTANT:
Do NOT implement BOTH of these solutions! Only ONE of them.
The first approach is a schema change, which doesn't require any data changes. That change will allow your UPDATE to succeed.
The second approach requires that the schema remain the same, and depends on the existence (and enforcement) of the UNIQUE KEY on (id,type).
When you update your query, you're creating multiple rows with id = 2 and type = 20, which is not allowed due to your primary key constraint.
You should instead use a single column as your primary key and have it auto-increment when you insert a new row. This way it's guaranteed to be unique.
You've set primary key on two columns, and after update the third row primary key will be the same as the second, so that is not allowed.
In the end you'll have 1..20; 2..20, 2..20, etc.

Categories