<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Cloud Connected &#187; mysql</title>
	<atom:link href="http://www.cloudconnected.fr/tag/mysql/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.cloudconnected.fr</link>
	<description>Thoughts of a french web developer</description>
	<lastBuildDate>Wed, 01 Feb 2012 08:53:57 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.3.1</generator>
		<item>
		<title>Trees in SQL : an approach based on materialized paths and normalization for MySQL</title>
		<link>http://www.cloudconnected.fr/2009/05/26/trees-in-sql-an-approach-based-on-materialized-paths-and-normalization-for-mysql/</link>
		<comments>http://www.cloudconnected.fr/2009/05/26/trees-in-sql-an-approach-based-on-materialized-paths-and-normalization-for-mysql/#comments</comments>
		<pubDate>Tue, 26 May 2009 16:13:11 +0000</pubDate>
		<dc:creator>Rémi</dc:creator>
				<category><![CDATA[Non classé]]></category>
		<category><![CDATA[mysql]]></category>

		<guid isPermaLink="false">http://www.the-asw.com/?p=534</guid>
		<description><![CDATA[I&#8217;m working since a few months on how storing trees in MySQL. It started like &#8220;hey, let&#8217;s make a database of every genre of heavy metal music!&#8221;. So I began with the easy way : a tree structure in a table genre with id and parent_id columns. And then I came to the point where [...]]]></description>
			<content:encoded><![CDATA[<p>I&#8217;m working since a few months on how storing trees in MySQL. It started like &#8220;hey, let&#8217;s make a database of every genre of heavy metal music!&#8221;. So I began with the easy way : a tree structure in a table <code>genre</code> with <code>id</code> and <code>parent_id</code> columns. And then I came to the point where I wanted to display the entire tree. This is just impossible with a reasonable number of queries, as there is no recursive syntax in standard SQL nor in MySQL (Oracle has the <code>CONNECT BY</code> extension). So I started researching the web.</p>
<p>There are basically three ways to store a tree in a relationnal databases : <strong>adjacency list</strong>,  <strong>nested set</strong> and <strong>materialized path</strong>, plus a few more like <strong>nested intervals</strong>. Let&#8217;s put it clear : they all suck.</p>
<ul>
<li><strong>Adjacency list</strong> model is the one I used the first time (with <code>parent_id</code> column). It&#8217;s easy to create but impossible to query deeply without recursivity.</li>
<li><strong>Nested set</strong> model is quite easy to query, but is indeed pretty fucked up (tree is limited in size, very hard to read without maths, almost all the lines of the table need to be updated each time a node is added, moved or deleted!). <strong>Nested intervals</strong> model tries to remove the size limitation of <strong>nested sets</strong> model, but involves way to much maths.</li>
<li><strong>Materialized path</strong> is great and easy to understand, but very slow because it involves heavy use of the &#8220;LIKE&#8221; operator.</ul>
<p>More on that <a href="http://dev.mysql.com/tech-resources/articles/hierarchical-data.html">here</a>, <a href="http://www.dbazine.com/oracle/or-articles/tropashko4">here</a> and <a href="http://www.ibstaff.net/fmartinez/?p=18">here</a>.</p>
<p>Then I found two very interesting articles. One is <a href="http://forums.mysql.com/read.php?125,101885,101954#msg-101954">a reply on MySQL Forums about materialized path performance problem</a>, recommending an approach based on normalized schema to store paths. The other is an <a href="http://www.depesz.com/index.php/2008/04/11/my-take-on-trees-in-sql/">implementation using PostgreSQL by depesz</a> that also use a normalized schema. The point is to create a table dedicated to store all the paths from every nodes to every nodes. This looks to me like a very good approach, as it&#8217;s easy to understand, easy to maintain, easy to query and performance-ok (with appropriate indexes). Unfortunalty, the implementation I found relies to much on PostgreSQL and doesn&#8217;t work out-of-the-box with MySQL (because of the use of triggers and of some queries that needs to be rewriten). So I reworked it to work with MySQL 5, and changed a few things. I strongly recommend that you read <a href="http://www.depesz.com/index.php/2008/04/11/my-take-on-trees-in-sql/">depesz&#8217;s post</a> for a complete understanding of the approach before you continue.</p>
<p><span id="more-534"></span></p>
<h2>Data Model</h2>
<p>First, we create a table containing the data, let&#8217;s name it <code>data</code>. The <code>parent_id</code> column stores the parent&#8217;s ID, like in the traditionnal adjacency list approach, but we&#8217;re not going to use it for querying the tree. The <code>value</code> column is whatever you need to store as value for the node, and we&#8217;re going to use it for ordering. We use InnoDB as storage engine.</p>
<pre>
CREATE TABLE data (
	id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
	parent_id INTEGER UNSIGNED null,
	value VARCHAR(255) NOT NULL,

	PRIMARY KEY (id),

	INDEX data_data_fk (parent_id),
	FOREIGN KEY data_data_fk (parent_id) REFERENCES data (id) ON DELETE CASCADE
) ENGINE = InnoDB;
</pre>
<p>Then we create the table that will store every paths. A path contains a parent node, a child node, and the number of hops between the two nodes (depth). Each node has at least one entry on that table: the path to itself with a depth egal 0. The <code>path</code> column will hold the materialized path, that will be used for ordering. If you&#8217;re absolutely sure that you will never ever need to display your tree in alphabetical order, then you can safely remove this column and remove related queries from triggers.</p>
<pre>
CREATE TABLE data_tree (
	parent_id INTEGER UNSIGNED NOT NULL,
	child_id INTEGER UNSIGNED NOT NULL,
	depth INTEGER UNSIGNED NOT NULL default 0,
	path TEXT NOT NULL,

	PRIMARY KEY (parent_id, child_id),
	INDEX data_tree_child_depth_idx (child_id, depth),

	INDEX data_tree_parent_data_fk (parent_id),
	FOREIGN KEY data_tree_parent_data_fk (parent_id) REFERENCES data (id) ON DELETE CASCADE,
	INDEX data_tree_child_data_fk (child_id),
	FOREIGN KEY data_tree_child_data_fk (child_id) REFERENCES data (id) ON DELETE CASCADE,
);
</pre>
<h2>Creating a new node</h2>
<p>When a node is inserted in the table <code>data</code>, then every path leading to this node has to be inserted in the table <code>data_tree</code>. To achieve this, I use a trigger (MySQL 5 or higher) because it&#8217;s safer for the DB&#8217;s integrity, but a client side implementation (in PHP or any other langage) will do, as long as you use a transaction.</p>
<pre>
DELIMITER |
CREATE TRIGGER insert_data
AFTER INSERT ON data
FOR EACH ROW BEGIN
	INSERT INTO data_tree (parent_id, child_id, depth, path)
	VALUES (NEW.id, NEW.id, 0, CONCAT(NEW.value,"/"));

	INSERT INTO data_tree (parent_id, child_id, depth, path)
		SELECT parent_id, NEW.id, depth + 1, CONCAT(path, NEW.value, "/")
		FROM data_tree
		WHERE child_id = NEW.parent_id;
END; |
DELIMITER ;
</pre>
<h2>Updating the tree</h2>
<p>First possible update : moving a node. To move a node, we basically have to delete every path between the node&#8217;s ancestors (if any) to the node&#8217;s childs (if any), and then insert all the new paths. This can be done easily with two queries in a trigger, or again, if you prefer, client side. No big changes from <a href="http://www.depesz.com/index.php/2008/04/11/my-take-on-trees-in-sql/">depesz&#8217;s post</a>, I only had to rewrite queries for MySQL.</p>
<p>Second possible update : changing a node&#8217;s value. This step is optionnal if you decided to drop the <code>path</code> column. When a node&#8217;s value is updated, for exemple from &#8220;Apinat&#8221; to &#8220;Banaanit&#8221;, then every path including that node value has to be updated as well, otherwise the tree won&#8217;t be correctly ordered. This step has to be completly rewriten for MySQL, has the original <a href="http://www.depesz.com/index.php/2008/04/11/my-take-on-trees-in-sql/">depesz&#8217;s implementation for PostgreSQL</a> uses regexp replacements, which are not supported. Hence I came with a workaround, using only pure string functions to make the replacement.</p>
<pre>
DELIMITER |
CREATE TRIGGER update_data
AFTER UPDATE ON data
FOR EACH ROW BEGIN
	DECLARE old_path_len INT;
	IF NEW.parent_id != OLD.parent_id OR NEW.parent_id IS NULL OR OLD.parent_id IS NULL THEN
		if OLD.parent_id IS NOT NULL THEN
			DELETE t2
			FROM data_tree t1
			JOIN data_tree t2 ON t1.child_id = t2.child_id
			WHERE t1.parent_id = OLD.id
			AND t2.depth > t1.depth;
		END IF;

		IF NEW.parent_id IS NOT NULL THEN
			INSERT INTO data_tree (parent_id, child_id, depth, path)
				SELECT t1.parent_id, t2.child_id, t1.depth + t2.depth + 1, CONCAT(t1.path, t2.path)
				FROM data_tree t1, data_tree t2
				WHERE t1.child_id = NEW.parent_id
				AND t2.parent_id = OLD.id;
		END IF;
	END IF;

	IF NEW.value != OLD.value THEN
		SELECT CHAR_LENGTH(path) INTO old_path_len
		FROM data_tree
		WHERE child_id = OLD.id
		AND DEPTH = 0;

		IF old_path_len > 0 THEN
			UPDATE data_tree t1
			JOIN data_tree t2 ON t1.child_id = t2.child_id
			SET t2.path = CONCAT(
				SUBSTRING(t2.path, 1, CHAR_LENGTH(t2.path) - CHAR_LENGTH(t1.path)),
				CONCAT(NEW.value, SUBSTRING(t1.path, old_path_len))
			)
			WHERE t1.parent_id = OLD.id
			AND t2.depth >= t1.depth;
		END IF;
	END IF;
END; |
DELIMITER ;
</pre>
<h2>Deleting a node</h2>
<p>Thanks to the <code>ON DELETE CASCADE</code> clause, we don&#8217;t need to do anything when deleting a node. Beware that every childs will also be deleted from the tree <strong>and</strong> from the <code>data</code> table.</p>
<h2>Examples</h2>
<p>Let&#8217;s play a little with this implementation.</p>
<h3>Populating the tree</h3>
<pre>
mysql> INSERT INTO data (parent_id, value) VALUES (null, 'Heavy metal');
Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM data;
+----+-----------+-------------+
| id | parent_id | value       |
+----+-----------+-------------+
|  1 |      NULL | Heavy metal |
+----+-----------+-------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM data_tree;
+-----------+----------+-------+--------------+
| parent_id | child_id | depth | path         |
+-----------+----------+-------+--------------+
|         1 |        1 |     0 | Heavy metal/ |
+-----------+----------+-------+--------------+
1 row in set (0.00 sec)

mysql> INSERT INTO data (parent_id, value) VALUES (1, 'Power metal'), (1, 'Trash metal'), (1, 'Death metal');
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> SELECT * FROM data;
+----+-----------+-------------+
| id | parent_id | value       |
+----+-----------+-------------+
|  1 |      NULL | Heavy metal |
|  2 |         1 | Power metal |
|  3 |         1 | Trash metal |
|  4 |         1 | Death metal |
+----+-----------+-------------+
4 rows in set (0.00 sec)

mysql> SELECT * FROM data_tree;
+-----------+----------+-------+--------------------------+
| parent_id | child_id | depth | path                     |
+-----------+----------+-------+--------------------------+
|         1 |        1 |     0 | Heavy metal/             |
|         1 |        2 |     1 | Heavy metal/Power metal/ |
|         1 |        3 |     1 | Heavy metal/Trash metal/ |
|         1 |        4 |     1 | Heavy metal/Death metal/ |
|         2 |        2 |     0 | Power metal/             |
|         3 |        3 |     0 | Trash metal/             |
|         4 |        4 |     0 | Death metal/             |
+-----------+----------+-------+--------------------------+
7 rows in set (0.00 sec)

mysql> INSERT INTO data (parent_id, value) VALUES (2, 'Symphonic power metal'), (2, 'Extreme power metal'), (4, 'Brutal death metal');
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> SELECT * FROM data_tree;
+-----------+----------+-------+------------------------------------------------+
| parent_id | child_id | depth | path                                           |
+-----------+----------+-------+------------------------------------------------+
|         1 |        1 |     0 | Heavy metal/                                   |
|         1 |        2 |     1 | Heavy metal/Power metal/                       |
|         1 |        3 |     1 | Heavy metal/Trash metal/                       |
|         1 |        4 |     1 | Heavy metal/Death metal/                       |
|         1 |        5 |     2 | Heavy metal/Power metal/Symphonic power metal/ |
|         1 |        6 |     2 | Heavy metal/Power metal/Extreme power metal/   |
|         1 |        7 |     2 | Heavy metal/Death metal/Brutal death metal/    |
|         2 |        2 |     0 | Power metal/                                   |
|         2 |        5 |     1 | Power metal/Symphonic power metal/             |
|         2 |        6 |     1 | Power metal/Extreme power metal/               |
|         3 |        3 |     0 | Trash metal/                                   |
|         4 |        4 |     0 | Death metal/                                   |
|         4 |        7 |     1 | Death metal/Brutal death metal/                |
|         5 |        5 |     0 | Symphonic power metal/                         |
|         6 |        6 |     0 | Extreme power metal/                           |
|         7 |        7 |     0 | Brutal death metal/                            |
+-----------+----------+-------+------------------------------------------------+
16 rows in set (0.00 sec)

mysql> SELECT * FROM data;
+----+-----------+-----------------------+
| id | parent_id | value                 |
+----+-----------+-----------------------+
|  1 |      NULL | Heavy metal           |
|  2 |         1 | Power metal           |
|  3 |         1 | Trash metal           |
|  4 |         1 | Death metal           |
|  5 |         2 | Symphonic power metal |
|  6 |         2 | Extreme power metal   |
|  7 |         4 | Brutal death metal    |
+----+-----------+-----------------------+
7 rows in set (0.00 sec)
</pre>
<h3>Requesting the tree</h3>
<h4>First generation of childs FROM &#8220;Heavy metal&#8221;</h4>
<pre>
mysql> SELECT d.id, d.value
       FROM data_tree t
       JOIN data d ON d.id = t.child_id
       WHERE t.parent_id = 1
       AND depth = 1;
+----+-------------+
| id | value       |
+----+-------------+
|  2 | Power metal |
|  3 | Trash metal |
|  4 | Death metal |
+----+-------------+
3 rows in set (0.00 sec)
</pre>
<h4>Printing the whole tree, ordered</h4>
<pre>
mysql> SELECT CONCAT(REPEAT('-',t.depth), d.value) AS value
       FROM data_tree t
       JOIN data d ON d.id = t.child_id
       WHERE t.parent_id = 1
       ORDER by t.path;
+-------------------------+
| value                   |
+-------------------------+
| Heavy metal             |
| -Death metal            |
| --Brutal death metal    |
| -Power metal            |
| --Extreme power metal   |
| --Symphonic power metal |
| -Trash metal            |
+-------------------------+
7 rows in set (0.00 sec)
</pre>
<h4>Ancestors of &#8220;Symphonic power metal&#8221;, starting from the root</h4>
<pre>
mysql> SELECT d.id, d.value
       FROM data_tree t
       JOIN data d ON d.id = t.parent_id
       WHERE t.child_id = 5
       ORDER BY depth DESC;
+----+-----------------------+
| id | value                 |
+----+-----------------------+
|  1 | Heavy metal           |
|  2 | Power metal           |
|  5 | Symphonic power metal |
+----+-----------------------+
3 rows in set (0.00 sec)
</pre>
<h3>Updating the tree</h3>
<h4>Moving a node to another place</h4>
<p>Let&#8217;s add a Hard rock in our tree, and move Heavy metal as a child of Hard rock.</p>
<pre>
mysql> INSERT INTO data (parent_id, value) VALUES (null, 'Hard rock');
Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM data;
+----+-----------+-----------------------+
| id | parent_id | value                 |
+----+-----------+-----------------------+
|  1 |      NULL | Heavy metal           |
|  2 |         1 | Power metal           |
|  3 |         1 | Trash metal           |
|  4 |         1 | Death metal           |
|  5 |         2 | Symphonic power metal |
|  6 |         2 | Extreme power metal   |
|  7 |         4 | Brutal death metal    |
|  8 |      NULL | Hard rock             |
+----+-----------+-----------------------+
8 rows in set (0.00 sec)

mysql> UPDATE data SET parent_id = 8 WHERE id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT * FROM data;
+----+-----------+-----------------------+
| id | parent_id | value                 |
+----+-----------+-----------------------+
|  1 |         8 | Heavy metal           |
|  2 |         1 | Power metal           |
|  3 |         1 | Trash metal           |
|  4 |         1 | Death metal           |
|  5 |         2 | Symphonic power metal |
|  6 |         2 | Extreme power metal   |
|  7 |         4 | Brutal death metal    |
|  8 |      NULL | Hard rock             |
+----+-----------+-----------------------+
8 rows in set (0.00 sec)

mysql> SELECT * FROM data_tree;
+-----------+----------+-------+----------------------------------------------------------+
| parent_id | child_id | depth | path                                                     |
+-----------+----------+-------+----------------------------------------------------------+
|         1 |        1 |     0 | Heavy metal/                                             |
|         1 |        2 |     1 | Heavy metal/Power metal/                                 |
|         1 |        3 |     1 | Heavy metal/Trash metal/                                 |
|         1 |        4 |     1 | Heavy metal/Death metal/                                 |
|         1 |        5 |     2 | Heavy metal/Power metal/Symphonic power metal/           |
|         1 |        6 |     2 | Heavy metal/Power metal/Extreme power metal/             |
|         1 |        7 |     2 | Heavy metal/Death metal/Brutal death metal/              |
|         2 |        2 |     0 | Power metal/                                             |
|         2 |        5 |     1 | Power metal/Symphonic power metal/                       |
|         2 |        6 |     1 | Power metal/Extreme power metal/                         |
|         3 |        3 |     0 | Trash metal/                                             |
|         4 |        4 |     0 | Death metal/                                             |
|         4 |        7 |     1 | Death metal/Brutal death metal/                          |
|         5 |        5 |     0 | Symphonic power metal/                                   |
|         6 |        6 |     0 | Extreme power metal/                                     |
|         7 |        7 |     0 | Brutal death metal/                                      |
|         8 |        1 |     1 | Hard rock/Heavy metal/                                   |
|         8 |        2 |     2 | Hard rock/Heavy metal/Power metal/                       |
|         8 |        3 |     2 | Hard rock/Heavy metal/Trash metal/                       |
|         8 |        4 |     2 | Hard rock/Heavy metal/Death metal/                       |
|         8 |        5 |     3 | Hard rock/Heavy metal/Power metal/Symphonic power metal/ |
|         8 |        6 |     3 | Hard rock/Heavy metal/Power metal/Extreme power metal/   |
|         8 |        7 |     3 | Hard rock/Heavy metal/Death metal/Brutal death metal/    |
|         8 |        8 |     0 | Hard rock/                                               |
+-----------+----------+-------+----------------------------------------------------------+
24 rows in set (0.00 sec)

mysql> SELECT CONCAT(REPEAT('-',t.depth), d.value) AS value
       FROM data_tree t
       JOIN data d ON d.id = t.child_id
       WHERE t.parent_id = 8
       ORDER BY t.path;
+--------------------------+
| value                    |
+--------------------------+
| Hard rock                |
| -Heavy metal             |
| --Death metal            |
| ---Brutal death metal    |
| --Power metal            |
| ---Extreme power metal   |
| ---Symphonic power metal |
| --Trash metal            |
+--------------------------+
8 rows in set (0.00 sec)
</pre>
<h4>Changing a node&#8217;s value</h4>
<p>Ok, I don&#8217;t know why we would do that, but let&#8217;s just rename Death metal to ZDeath metal and see what happens.</p>
<pre>
mysql> UPDATE data SET value = 'ZDeath metal' WHERE id = 4;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT * FROM data_tree;
+-----------+----------+-------+----------------------------------------------------------+
| parent_id | child_id | depth | path                                                     |
+-----------+----------+-------+----------------------------------------------------------+
|         1 |        1 |     0 | Heavy metal/                                             |
|         1 |        2 |     1 | Heavy metal/Power metal/                                 |
|         1 |        3 |     1 | Heavy metal/Trash metal/                                 |
|         1 |        4 |     1 | Heavy metal/ZDeath metal/                                |
|         1 |        5 |     2 | Heavy metal/Power metal/Symphonic power metal/           |
|         1 |        6 |     2 | Heavy metal/Power metal/Extreme power metal/             |
|         1 |        7 |     2 | Heavy metal/ZDeath metal/Brutal death metal/             |
|         2 |        2 |     0 | Power metal/                                             |
|         2 |        5 |     1 | Power metal/Symphonic power metal/                       |
|         2 |        6 |     1 | Power metal/Extreme power metal/                         |
|         3 |        3 |     0 | Trash metal/                                             |
|         4 |        4 |     0 | ZDeath metal/                                            |
|         4 |        7 |     1 | ZDeath metal/Brutal death metal/                         |
|         5 |        5 |     0 | Symphonic power metal/                                   |
|         6 |        6 |     0 | Extreme power metal/                                     |
|         7 |        7 |     0 | Brutal death metal/                                      |
|         8 |        1 |     1 | Hard rock/Heavy metal/                                   |
|         8 |        2 |     2 | Hard rock/Heavy metal/Power metal/                       |
|         8 |        3 |     2 | Hard rock/Heavy metal/Trash metal/                       |
|         8 |        4 |     2 | Hard rock/Heavy metal/ZDeath metal/                      |
|         8 |        5 |     3 | Hard rock/Heavy metal/Power metal/Symphonic power metal/ |
|         8 |        6 |     3 | Hard rock/Heavy metal/Power metal/Extreme power metal/   |
|         8 |        7 |     3 | Hard rock/Heavy metal/ZDeath metal/Brutal death metal/   |
|         8 |        8 |     0 | Hard rock/                                               |
+-----------+----------+-------+----------------------------------------------------------+
24 rows in set (0.00 sec)

mysql> SELECT CONCAT(REPEAT('-',t.depth), d.value) AS value
       FROM data_tree t
       JOIN data d ON d.id = t.child_id
       WHERE t.parent_id = 8
       ORDER by t.path;
+--------------------------+
| value                    |
+--------------------------+
| Hard rock                |
| -Heavy metal             |
| --Power metal            |
| ---Extreme power metal   |
| ---Symphonic power metal |
| --Trash metal            |
| --ZDeath metal           |
| ---Brutal death metal    |
+--------------------------+
8 rows in set (0.00 sec)
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.cloudconnected.fr/2009/05/26/trees-in-sql-an-approach-based-on-materialized-paths-and-normalization-for-mysql/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Quel est le meilleur type pour stocker un mot de passe dans une table MySQL ?</title>
		<link>http://www.cloudconnected.fr/2009/04/12/quel-est-le-meilleur-type-pour-stocker-un-mot-de-passe-dans-une-table-mysql/</link>
		<comments>http://www.cloudconnected.fr/2009/04/12/quel-est-le-meilleur-type-pour-stocker-un-mot-de-passe-dans-une-table-mysql/#comments</comments>
		<pubDate>Sun, 12 Apr 2009 13:44:05 +0000</pubDate>
		<dc:creator>Rémi</dc:creator>
				<category><![CDATA[Non classé]]></category>
		<category><![CDATA[mysql]]></category>

		<guid isPermaLink="false">http://www.the-asw.com/?p=461</guid>
		<description><![CDATA[D&#8217;abord, il va de soi (mais ça va mieux en le disant) que les mots de passe d&#8217;une application web ne doivent pas être stockés en clair, mais sous forme d&#8217;un hash. Lors de la procédure d&#8217;authentification, il suffit de calculer le hash du mot de passe saisi par l&#8217;utilisateur, et de le comparer avec [...]]]></description>
			<content:encoded><![CDATA[<p>D&#8217;abord, il va de soi (mais ça va mieux en le disant) que les mots de passe d&#8217;une application web ne doivent pas être stockés en clair, mais sous forme d&#8217;un hash. Lors de la procédure d&#8217;authentification, il suffit de calculer le hash du mot de passe saisi par l&#8217;utilisateur, et de le comparer avec celui stocké en base.</p>
<p>L&#8217;algorithme le plus répandu pour calculer un hash est MD5. Néanmoins, depuis 2004 plusieurs vulnérabilités ont été découvertes, aussi il est préférable d&#8217;utiliser SHA1. Quel est meilleur type pour stocker ces hashs dans une table MySQL ?</p>
<p>Un hash SHA1 mesure 160 bits ou 40 caractères hexadécimaux. Un hash MD5 mesure 128 bits soit 32 caractères hexadécimaux.</p>
<h3>Stocker sous forme de chaine</h3>
<p>La longueur du hash est fixe : il est donc préférable d&#8217;utiliser le type <code>CHAR</code> (<code>CHAR(40)</code> pour du SHA1, <code>CHAR(32)</code> pour du MD5). En effet, le type <code>VARCHAR</code> prendrait plus d&#8217;espace car il utilise un octet supplémentaire pour stocker la longueur de la chaine.</p>
<p>Ensuite, il faut faire attention à l&#8217;encodage. En effet, si la table est en UTF-8, les caractères seront probablement stockés sur 3 octets (ça peut varier selon le moteur de stockage). Il vaut mieux spécifier l&#8217;encodage de cette colonne à <code>ASCII</code>, grâce à <code>CHARACTER SET ascii</code>.</p>
<p>Enfin, la colonne est évidemment <code>NOT NULL</code>, pour économiser un bit (la valeur <code>NULL</code> est stockée dans un bit supplémentaire), à moins que l&#8217;application en ait besoin (ça existe des utilisateurs sans mot de passe ?).</p>
<p>Exemple :</p>
<pre>
mysql> create table t (
    -> password char(40) character set ascii not null
    -> ) engine=MyISAM character set utf8;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into t values(SHA1('monSuperMotDePasse'));
Query OK, 1 row affected (0.00 sec)

mysql> select * from t;
+------------------------------------------+
| password                                 |
+------------------------------------------+
| 35ea5a462298bd78c1648fbea2195988c46d103f |
+------------------------------------------+
1 row in set (0.00 sec)
</pre>
<p>L&#8217;espace utilisé est donc 40 octets pour un hash SHA1, car le type CHAR avec le jeu de caractère ASCII stocke chaque caractère sur un octet (8 bits).</p>
<h3>Stocker sous forme binaire</h3>
<p>Vous l&#8217;aurez surement remarqué, 40 octets ça fait 320 bits, c&#8217;est à dire le double de la taille du hash SHA1 brut (idem pour MD5 : 32&#215;8 = 256). Pour faire simple, c&#8217;est le prix à payer pour avoir un mot de passe lisible et &#8220;imprimable&#8221; (c&#8217;est à dire que vous pouvez copier/coller sans problème) en base. L&#8217;autre solution consiste à stocker le hash brut directement sous forme binaire.</p>
<p>Le type a utilisé n&#8217;est donc plus <code>CHAR</code>, mais <code>BINARY</code> qui stocke des chaines binaires. Il faut lui préciser la longueur en octets, donc <code>BINARY(20)</code> pour du SHA1 (160/8 = 20), et <code>BINARY(16)</code> pour du MD5 (128/8 = 16). Il n&#8217;y a plus à se soucier du jeu de caractères.</p>
<p>Pour convertir une chaine de caractère en chaine binaire, il faut utiliser <code>UNHEX</code>. Pour l&#8217;opération inverse, il faut utiliser <code>HEX</code>. Note pour les utilisateurs de PHP : la fonction <code>hash</code> accepte un troisième paramètre optionnel <code>$raw_output</code> qui, s&#8217;il vaut <code>true</code>, permet de récupérer le résultat binaire directement (voir <a href="http://fr.php.net/manual/fr/function.hash.php">http://fr.php.net/manual/fr/function.hash.php</a>).</p>
<p>Exemple :</p>
<pre>
mysql> create table t2 (
    -> password binary(20) not null
    -> ) engine=MyIsam character set utf8;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into t2 values(UNHEX(SHA1('monSuperMotDePasse')));
Query OK, 1 row affected (0.00 sec)

mysql> select HEX(password) from t2;
+------------------------------------------+
| HEX(password)                            |
+------------------------------------------+
| 35EA5A462298BD78C1648FBEA2195988C46D103F |
+------------------------------------------+
1 row in set (0.00 sec)
</pre>
<p>L&#8217;espace utilisé est donc 20 octets pour hash SHA1, soit 2 fois moins que la version en chaine de caractères. L&#8217;inconvénient est qu&#8217;il faut penser à utiliser <code>UNHEX</code> ou <code>HEX</code> dès qu&#8217;on veut le hash sous une forme imprimable. C&#8217;est rarement le cas, et comme les opérations de comparaisons (utilisées pour l&#8217;authentification de l&#8217;utilisateur) peuvent s&#8217;effectuer avec les version binaires, ça ne me dérange pas.</p>
<h3>En savoir plus</h3>
<ul>
<li><a href="http://dev.mysql.com/doc/refman/5.0/en/char.html">The CHAR and VARCHAR Types</a></li>
<li><a href="http://dev.mysql.com/doc/refman/5.0/en/charset-column.html">Column Character Set and Collation</a></li>
<li><a href="http://dev.mysql.com/doc/refman/5.0/en/binary-varbinary.html">The BINARY and VARBINARY Types</a></li>
<li><a href="http://dev.mysql.com/doc/refman/5.0/en/data-size.html">Make Your Data as Small as Possible</a></li>
</ul>
]]></content:encoded>
			<wfw:commentRss>http://www.cloudconnected.fr/2009/04/12/quel-est-le-meilleur-type-pour-stocker-un-mot-de-passe-dans-une-table-mysql/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>MySQL INT(11) a la même taille que INT(3)</title>
		<link>http://www.cloudconnected.fr/2009/04/09/mysql-int11-a-la-meme-taille-que-int3/</link>
		<comments>http://www.cloudconnected.fr/2009/04/09/mysql-int11-a-la-meme-taille-que-int3/#comments</comments>
		<pubDate>Thu, 09 Apr 2009 12:14:12 +0000</pubDate>
		<dc:creator>Rémi</dc:creator>
				<category><![CDATA[Non classé]]></category>
		<category><![CDATA[mysql]]></category>

		<guid isPermaLink="false">http://www.the-asw.com/?p=456</guid>
		<description><![CDATA[Vous ne le saviez peut-être pas, mais contrairement aux types de chaine de caractères (varchar, char, &#8230;), le chiffre entre parenthèses pour un type numérique n&#8217;a aucune influence sur la taille maximale du type. Autrement dit, on peut stocker le même nombre dans un int(11) que dans un int(3), un int ou même un int(42). [...]]]></description>
			<content:encoded><![CDATA[<p>Vous ne le saviez peut-être pas, mais contrairement aux types de chaine de caractères (<code>varchar</code>, <code>char</code>, &#8230;), le chiffre entre parenthèses pour un type numérique n&#8217;a aucune influence sur la taille maximale du type. Autrement dit, on peut stocker le même nombre dans un <code>int(11)</code> que dans un <code>int(3)</code>, un <code>int</code> ou même un <code>int(42)</code>. Ce qui influe la taille maximale, c&#8217;est le type en lui même : <code>tinyint</code>, <code>smallint</code>, <code>mediumint</code>, <code>int</code> et <code>bigint</code>.</p>
<p>Tableau de correspondance des tailles (source : <a href="http://dev.mysql.com/doc/refman/5.1/en/numeric-types.html">http://dev.mysql.com/doc/refman/5.1/en/numeric-types.html</a>)</p>
<table cellspacing="0">
<thead>
<tr>
<th>Type</th>
<th>Bytes</th>
<th>Minimum Value (Signed/Unsigned)</th>
<th>Maximum Value (Signed/Unsigned)</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="2">TINYINT</td>
<td rowspan="2">1</td>
<td>-128</td>
<td>127</td>
</tr>
<tr>
<td>0</td>
<td>255</td>
</tr>
<tr>
<td rowspan="2">SMALLINT </td>
<td rowspan="2">2</td>
<td>-32768</td>
<td>32767</td>
</tr>
<tr>
<td>0</td>
<td>65535</td>
</tr>
<tr>
<td rowspan="2">MEDIUMINT</td>
<td rowspan="2">3</td>
<td>-8388608</td>
<td>8388607</td>
</tr>
<tr>
<td>0</td>
<td>16777215</td>
</tr>
<tr>
<td rowspan="2">INT</td>
<td rowspan="2">4</td>
<td>-2147483648</td>
<td>2147483647</td>
</tr>
<tr>
<td>0</td>
<td>4294967295</td>
</tr>
<tr>
<td rowspan="2">BIGINT</td>
<td rowspan="2">8</td>
<td>-9223372036854775808</td>
<td>9223372036854775807</td>
</tr>
<tr>
<td>0</td>
<td>18446744073709551615</td>
</tr>
</tbody>
</table>
<p>Alors quel est l&#8217;intérêt de la préciser une taille ? Il n&#8217;y en a qu&#8217;un seul, c&#8217;est lorsque le type est utilisé avec l&#8217;option <code>zerofill</code>, option assez peu connue qui permet de compléter avec le champ avec des zéros pour atteindre la taille spécifiée.</p>
<p>Exemple :</p>
<pre>
mysql> create table test_int (
    -> normal_int int unsigned not null,
    -> zerofilled_int int(6) unsigned zerofill not null
    -> );
Query OK, 0 rows affected (0.00 sec)

mysql> insert into test_int values (123456789, 123456789), (42,42);
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> select * from test_int;
+------------+----------------+
| normal_int | zerofilled_int |
+------------+----------------+
|  123456789 |      123456789 |
|         42 |         000042 |
+------------+----------------+
2 rows in set (0.00 sec)
</pre>
<p>Comme vous pouvez le constater dans l&#8217;exemple précédent, il n&#8217;y aucune différence sur la première ligne (un nombre composé de 9 chiffres tient sans problème dans un <code>int(6)</code>), par contre dans la seconde, le champ avec l&#8217;option <code>zerofill</code> est complété par des zéros pour atteindre 6 chiffres. Un exemple concret d&#8217;utilisation : stocker des numéros de série normalisé (genre des numéros de bon de commande du type &#8220;BDC-000042&#8243;), car ça évite de faire un padding.</p>
<p>Bref, tout ça pour dire : ça ne sert à rien d&#8217;essayer d&#8217;optimiser ses tables MySQL avec ça, ce n&#8217;est pas en mettant <code>int(4)</code> qu&#8217;on va gagner de la place, mais plutôt en utilisant le type adapté (en l&#8217;occurence <code>smallint</code>).</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cloudconnected.fr/2009/04/09/mysql-int11-a-la-meme-taille-que-int3/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Internationalisation d&#8217;une base de données</title>
		<link>http://www.cloudconnected.fr/2009/03/06/internationalisation-dune-base-de-donnees/</link>
		<comments>http://www.cloudconnected.fr/2009/03/06/internationalisation-dune-base-de-donnees/#comments</comments>
		<pubDate>Fri, 06 Mar 2009 15:19:16 +0000</pubDate>
		<dc:creator>Rémi</dc:creator>
				<category><![CDATA[Non classé]]></category>
		<category><![CDATA[i18n]]></category>
		<category><![CDATA[mysql]]></category>
		<category><![CDATA[pdo]]></category>
		<category><![CDATA[php]]></category>

		<guid isPermaLink="false">http://www.the-asw.com/?p=407</guid>
		<description><![CDATA[Disons que vous soyez en charge d&#8217;une application web écrite en PHP/MySQL, par exemple l&#8217;intranet de votre entreprise, et que vous ayez soudainement besoin de l&#8217;internationaliser parce que votre entreprise installe des bureaux à l&#8217;étranger et que, malheureusement, notre merveilleuse langue française n&#8217;est pas parlée dans tous les pays du monde. Pour les templates, pas [...]]]></description>
			<content:encoded><![CDATA[<p>Disons que vous soyez en charge d&#8217;une application web écrite en PHP/MySQL, par exemple l&#8217;intranet de votre entreprise, et que vous ayez soudainement besoin de l&#8217;internationaliser parce que votre entreprise installe des bureaux à l&#8217;étranger et que, malheureusement, notre merveilleuse langue française n&#8217;est pas parlée dans tous les pays du monde.</p>
<p>Pour les templates, pas de problème, <code>gettext</code> est là pour ça (je reviendrais peut-être dessus dans un futur article, si je suis motivé). Si vous n&#8217;avez pas envie de mettre les mains dans le système, votre framework propose sûrement une émulation plus ou moins performante en pur PHP, ou, au pire, une solution &#8220;maison&#8221;. Bref, ça c&#8217;est facile.</p>
<p>Ce qui pose plus de problèmes, ce sont les données présentes en base. Par exemple : la liste de catégories pour les tickets d&#8217;incidents des clients. Elle est stockée en base dans une table qui contient notamment l&#8217;intitulé de cette catégorie. Oui, mais cet intitulé doit être traduit. Et comme les catégories sont gérées dynamiquement, ce n&#8217;est pas envisage d&#8217;utiliser la méthode <code>gettext</code> qui repose sur des fichiers statiques.</p>
<h3>Transformer le modèle</h3>
<p>Nous allons commencer par ajouter une table pour stocker les traductions. Si on envisage la catégorie comme un élément identifié par un id uniquement, les traductions sont liées à une catégorie par une relation &#8220;1-N&#8221; : 1 catégorie possède N traductions. Chaque traduction est identifiée par un code de langue. L&#8217;idée est de retirer les champs contenant du texte à traduire de la catégorie, et de les stocker dans la table contenant les traductions</p>
<div id="attachment_408" class="wp-caption aligncenter" style="width: 395px"><a href="http://www.cloudconnected.fr/wp-content/uploads/2009/03/i18n.png"  rel="lightbox"><img src="http://www.cloudconnected.fr/wp-content/uploads/2009/03/i18n.png" alt="Schéma de la base" title="i18n" width="385" height="142" class="size-full wp-image-408" /></a><p class="wp-caption-text">Schéma de la base</p></div>
<p>On crée donc les tables suivantes :</p>
<pre>
CREATE TABLE `category` (
	`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
	`created_at` DATETIME NOT NULL,
	`deleted_at` DATETIME NULL,
	PRIMARY KEY (`id`)
) ENGINE = InnoDB;

CREATE TABLE `category_i18n` (
	`category_id` INT UNSIGNED NOT NULL,
	`lang` VARCHAR(6) NOT NULL,
	`name` VARCHAR(45) NOT NULL,
	PRIMARY KEY (`lang`, `category_id`),
	INDEX `category_i18n_category_fk` (`category_id` ASC),
	CONSTRAINT `category_i18n_category_fk`
		FOREIGN KEY (`category_id` )
		REFERENCES `category` (`id` )
		ON DELETE CASCADE
) ENGINE = InnoDB;
</pre>
<p><span id="more-407"></span></p>
<h3>Insertion</h3>
<p>Lors de l&#8217;insertion, il faut d&#8217;abord insérer dans <code>category</code>, récupérer l&#8217;id puis insérer dans <code>category_i18n</code>. Puisqu&#8217;on travaille avec plusieurs tables en même temps, l&#8217;utilisation d&#8217;une transaction (avec le moteur de stockage InnoDB) est fortement recommandée.</p>
<pre>
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO category (created_at) VALUES (SYSDATE());
Query OK, 1 row affected (0.00 sec)

mysql> SET @category_id = LAST_INSERT_ID();
Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO category_i18n (category_id, lang, name) VALUES
    -> (@category_id, 'fr', 'Incident réseau'),
    -> (@category_id, 'en', 'The network is fucked up');
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)
</pre>
<p>Et ça donne :</p>
<pre>
mysql> SELECT * FROM category;
+----+---------------------+------------+
| id | created_at          | deleted_at |
+----+---------------------+------------+
|  3 | 2009-03-06 15:58:44 | NULL       |
+----+---------------------+------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM category_i18n;
+-------------+------+--------------------------+
| category_id | lang | name                     |
+-------------+------+--------------------------+
|           3 | en   | The network is fucked up |
|           3 | fr   | Incident réseau          |
+-------------+------+--------------------------+
2 rows in set (0.00 sec)
</pre>
<h4>Exemple d&#8217;implémentation en utilisant PDO</h4>
<pre>
// $names contient un tableau associatif "langue => valeur"
function newCategory($dbh, $names = array())
{
	if ( empty($names) )
		return false;

	try {
		$dbh->beginTransaction();

		$sql =
			'INSERT INTO category
			(created_at)
			VALUES (SYSDATE())';

		$dbh->exec($sql);
		$id = $dbh->lastInsertId();

		$sql = array();
		foreach ( $names as $lang => $name ) {
			$sql[] = sprintf(
				'(%d, %s, %s)',
				$id,
				$dbh->quote($lang),
				$dbh->quote($name)
			);
		}
		$sql =
			'INSERT INTO category_i18n
			(category_id, lang, name)
			VALUES '.implode(', ',$sql);
		$dbh->exec($sql);

		$dbh->commit();
	} catch (Exception $e) {
		$dbh->rollback();
		throw $e;
	}
}

// appel de la fonction
$dbh = new PDO('mysql:host=localhost;dbname=test', 'test', 'test');
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

newCategory($dbh, array(
	'fr' => 'Incident réseau',
	'en' => 'The network is fucked up'
));
newCategory($dbh, array(
	'fr' => 'Incident système',
	'en' => 'The system screwed up'
));
newCategory($dbh, array(
	'fr' => 'Commande de matériel',
	'en' => 'I need some stuffs'
));
newCategory($dbh, array(
	'fr' => 'Problème typiquement français'
));
</pre>
<h3>Sélection</h3>
<p>Pour récupérer la liste des catégories, il faut savoir dans quelle langue on les veut. Pour cela, on peut se baser sur les préférences indiquée par le navigateur de l&#8217;utilisateur, ou sur ses options s&#8217;il est inscrit, etc. La requête de sélection se fait ensuite par une simple jointure.</p>
<p>Dans l&#8217;exemple suivant, on sélectionne d&#8217;abord les catégories en français et ensuite les catégories en anglais. La catégorie non-traduite n&#8217;apparaît pas. On pourrait utiliser un <code>LEFT JOIN</code> pour avoir un libellé <code>NULL</code> (ça peut servir dans certains cas).</p>
<pre>
mysql> SELECT c.id, ci.name
    -> FROM category c
    -> JOIN category_i18n ci ON (ci.category_id = c.id AND ci.lang = "fr")
    -> WHERE c.deleted_at IS NULL;
+----+-------------------------------+
| id | name                          |
+----+-------------------------------+
|  1 | Incident réseau               |
|  2 | Incident système              |
|  3 | Commande de matériel          |
|  4 | Problème typiquement français |
+----+-------------------------------+
4 rows in set (0.00 sec)

mysql> SELECT c.id, ci.name
    -> FROM category c
    -> JOIN category_i18n ci ON (ci.category_id = c.id AND ci.lang = "en")
    -> WHERE c.deleted_at IS NULL;
+----+--------------------------+
| id | name                     |
+----+--------------------------+
|  1 | The network is fucked up |
|  2 | The system screwed up    |
|  3 | I need some stuffs       |
+----+--------------------------+
3 rows in set (0.00 sec)

mysql> SELECT c.id, ci.name
    -> FROM category c
    -> LEFT JOIN category_i18n ci ON (ci.category_id = c.id AND ci.lang = "en")
    -> WHERE c.deleted_at IS NULL;
+----+--------------------------+
| id | name                     |
+----+--------------------------+
|  1 | The network is fucked up |
|  2 | The system screwed up    |
|  3 | I need some stuffs       |
|  4 | NULL                     |
+----+--------------------------+
4 rows in set (0.00 sec)
</pre>
<h3>Modification</h3>
<p>La modification peut être triviale, ou un petit peu complexe, selon le type d&#8217;interface utilisateur mise en place. Quelques astuces : si un simple <code>UPDATE</code> ne suffit pas, vous devriez regarder du côté de <code>INSERT ... ON DUPLICATE KEY UPDATE ...</code>, ou implémenter une suppression complète immédiatement suivi de nouvelles insertions (toujours avec une transaction).</p>
<h3>Suppression</h3>
<p>Grâce à la clause <code>ON DELETE CASCADE</code> il suffit de supprimer l&#8217;entrée dans catégorie pour que les traductions correspondantes soient également supprimées. Personnellement j&#8217;utilise le champ <code>deleted_at</code> pour effectuer une suppression logique et ne pas casser toutes les clés étrangères des tables qui font référence à la catégorie supprimée.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cloudconnected.fr/2009/03/06/internationalisation-dune-base-de-donnees/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Développement web collaboratif</title>
		<link>http://www.cloudconnected.fr/2008/07/08/developpement-web-collaboratif/</link>
		<comments>http://www.cloudconnected.fr/2008/07/08/developpement-web-collaboratif/#comments</comments>
		<pubDate>Tue, 08 Jul 2008 13:47:14 +0000</pubDate>
		<dc:creator>Rémi</dc:creator>
				<category><![CDATA[Non classé]]></category>
		<category><![CDATA[apache]]></category>
		<category><![CDATA[gestion de projet]]></category>
		<category><![CDATA[mysql]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[samba]]></category>
		<category><![CDATA[svn]]></category>

		<guid isPermaLink="false">http://www.the-asw.com/?p=305</guid>
		<description><![CDATA[On me demande souvent comment j&#8217;organise le développement web dans mon entreprise : comment on arrive à coder à plusieurs sur le même fichier sans se marcher sur les pieds, comment on archive les versions, comment on effectue les livraisons en production, etc. Il est vrai que le développement web collaboratif est souvent mal organisé, [...]]]></description>
			<content:encoded><![CDATA[<p>On me demande souvent comment j&#8217;organise le développement web dans mon entreprise : comment on arrive à coder à plusieurs sur le même fichier sans se marcher sur les pieds, comment on archive les versions, comment on effectue les livraisons en production, etc. Il est vrai que le développement web collaboratif est souvent mal organisé, et il est courant d&#8217;avoir comme seul outil un simple dossier partagé&#8230;</p>
<h3>Contraintes liées au développement web collaboratif</h3>
<p>Contrairement à un projet classique qui ne nécessite &#8220;que&#8221; un compilateur et des bibliothèques, une application web est une association complexe de plusieurs composants logiciels dont les versions et les configurations très variables peuvent influer sur le bon fonctionnement de l&#8217;application :</p>
<ul>
<li>un serveur web (vhost, htaccess, mod_rewrite, etc.),</li>
<li>l&#8217;interpréteur php (options de php.ini, modules supplémentaires comme pdo, gettext, etc.),</li>
<li>le sgbd (configuration des users, etc.).</li>
</ul>
<p>Donc la solution &#8220;classique&#8221; d&#8217;avoir un poste de développement par développeur est ici difficile à maintenir : il faudrait installer tous ces logiciels sur chaque poste et surtout veiller à avoir une configuration identique partout. Et ça se complique dès qu&#8217;il faut faire une mise à jour de la config&#8230;</p>
<p>Une autre solution serait d&#8217;avoir un serveur de dev unique (pour éviter les problèmes de maintenance évoqués ci-avant), et de travailler directement dessus via des partages de fichiers (Samba, NFS, etc.). Or cette solution pose plusieurs problèmes :</p>
<ul>
<li>en cas d&#8217;accès concurrents à un fichier : le risque est grand de voir ses modifications écrasées par un autre développeur ;</li>
<li>en cas de modifications lourdes (exemple : un refactoring de la base) : les autres développeurs sont bloqués ;</li>
<li>impossible d&#8217;utiliser correctement le svn : il n&#8217;y a qu&#8217;une seule version des fichiers et on ne sait pas qui l&#8217;a modifiée.</li>
</ul>
<p>Nous avons donc créée une solution intermédiaire : un serveur de dev unique (pour une maintenance facile), qui fournit des serveurs virtuels pour chaque développeur (pour une bonne séparation des données et une utilisation optimale de svn).</p>
<p>Le serveur de dev fonctionne avec Debian GNU/Linux, les postes clients avec Windows XP.</p>
<p><span id="more-305"></span></p>
<h3>Plateforme technique</h3>
<h4>Compte sur le serveur</h4>
<p>Chaque développeur dispose d&#8217;un compte uniquement sur le serveur de dev, qui lui permet de se connecter dessus en SSH (bien utile pour tester des tâches en ligne de commande tel que des scripts de maintenance). De plus, tous les développeurs appartiennent au même groupe (<code>www</code>) et les permissions sont par défaut en <code>rw</code> pour le groupe.</p>
<h4>Partage Windows : Samba</h4>
<p>Grace à Samba, chaque développeur dispose d&#8217;un partage windows identifié par son login. Le partage est également accessible en local sur la machine, dans son <em>home</em>.</p>
<h4>Vhost Apache</h4>
<p>Pour chaque dossier créé dans le partage, il existe un vhost permettant d&#8217;y accéder, sous la forme <code>http://login.dossier.dev.bso</code>.</p>
<p>Exemple, je crée le dossier <code>monboprojet</code> dans mon partage <code>monbologin</code>. L&#8217;URL est : <code>http://monbologin.monboprojet.dev.bso</code></p>
<p>Pour les projets dont le <code>documentRoot</code> n&#8217;est pas à la racine (c&#8217;est à dire à peut près tous les projets sur svn, puisque la racine contient seulement <code>trunk/</code>, <code>branches/</code> et <code>tags/</code>), il vaut mieux les mettre dans un répertoire suffixé par &#8220;-src&#8221; et faire un lien symbolique. Par exemple :</p>
<pre>
$ svn checkout http://mon.beau.svn/monboprojet monboprojet-src
$ ln -s monboprojet-src/trunk/htdocs monboprojet
</pre>
<h4>BDD Mysql</h4>
<p>Il n&#8217;y a qu&#8217;un seul serveur MySQL. Chaque développeur peut disposer de son propre compte et de ses propres bases. Cependant, pour des raisons pratiques, on utilise la plupart du temps les mêmes bases. Un développeur ne va utiliser des bases à part que s&#8217;il a besoin de faire des opérations lourdes (genre modifier la structure d&#8217;un ensemble de tables) qui bloqueraient tout le monde le temps que le code mis à jour soit commit.</p>
<h3>Coordination du développement</h3>
<h4>SVN</h4>
<p>Un serveur Subversion (SVN) est utilisé pour gérer les sources. Le serveur svn est sur une machine différente du serveur de dev ce qui permet de sécuriser les données (en cas de crash de la machine de dev, on peut récupérer les dépots ; en cas de crash de la machine svn, on a toujours les versions locales).</p>
<p>Les dépots sont organisés de la manière classique avec 3 repertoires :</p>
<ul>
<li>branches/</li>
<li>trunk/</li>
<li>tags/</li>
</ul>
<p>Un commit ne doit pas casser le dépôt. C&#8217;est à dire qu&#8217;un développeur qui ferait un update doit pouvoir continuer à utiliser l&#8217;application sans être bloqué par un bug, une fonctionnalité manquante, ou autre. Il est tout à fait possible de faire un commit d&#8217;une feature non terminée, à partir du moment où le reste de l&#8217;application n&#8217;est pas impacté.</p>
<h4>Fichiers de config</h4>
<p>On ne commit jamais un fichier de config. En effet, d&#8217;une part un fichier de configuration est spécifique à un environnement (options de mise en cache, url, etc.) et d&#8217;autre part un fichier de configuration contient des logins et des mots de passes spécifiques.</p>
<p>On commit donc des modèles de fichiers de configuration, dont le nom se termine par .orig. Chaque développeur doit créer ses propres fichiers à partir des modèles. Quand on voit passer un &#8220;.orig&#8221; dans les logs d&#8217;update, on sait qu&#8217;il faut mettre à jour ce fichier. Cela est valable également pour les livraisons.</p>
<h4>SQL</h4>
<p>Pour permettre les livraisons (en préprod et en prod), à chaque modification de la base un fichier sql contenant les requêtes de mise à jour (alter, etc.) doit être écrit par le développeur. Pour éviter d&#8217;en avoir trop à gérer, il est intéressant de créer un fichier de mise à jour sql par version que l&#8217;on a prévu de livrer (<code>upgrade_1.0.sql</code>, <code>upgrade_1.1.sql</code>, etc.)</p>
<h4>Livraison</h4>
<p>Pour livrer, un développeur (en général le responsable du projet) utilise <code>rsync</code> depuis sa copie locale sur la machine de dev pour synchroniser les sources avec la préprod ou la prod. Puis il se connecte en SSH pour executer les scripts d&#8217;upgrade sql (d&#8217;où l&#8217;interet de les avoir écrit au fur et à mesure) et mettre à jour les fichiers de config.</p>
<p>Une option interessante de rsync est <code>--exclude-from=<em>nom_de_fichier</em></code>, qui permet d&#8217;exclure du rsync tous les fichiers ou les répertoires qui matchent les noms présents dans <code>nom_de_fichier</code>. Très pratique pour éviter de livrer les fichiers inutiles (.svn, .mo, &#8230;) et éviter d&#8217;écraser les fichiers de config&#8230; Exemple de fichier d&#8217;exclusion : </p>
<pre>
.svn
rsync*
doc*
config/*
Thumbs.db
*.mo
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.cloudconnected.fr/2008/07/08/developpement-web-collaboratif/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Une syntaxe SQL originale</title>
		<link>http://www.cloudconnected.fr/2007/11/22/une-syntaxe-sql-originale/</link>
		<comments>http://www.cloudconnected.fr/2007/11/22/une-syntaxe-sql-originale/#comments</comments>
		<pubDate>Thu, 22 Nov 2007 14:19:34 +0000</pubDate>
		<dc:creator>Rémi</dc:creator>
				<category><![CDATA[Non classé]]></category>
		<category><![CDATA[mysql]]></category>
		<category><![CDATA[sécurité web]]></category>

		<guid isPermaLink="false">http://www.the-asw.com/?p=301</guid>
		<description><![CDATA[La question à 2 centimes d&#8217;euros du jour : qu&#8217;est-ce qu&#8217;on obtient comme résultat avec une requête SQL de ce genre de celle ci-dessous (avec un MySQL) ? select * from ma_table where id = 42 = 0; La réponse est : toute la table sauf la ligne dont l&#8217;id vaut 42 ! En fait, [...]]]></description>
			<content:encoded><![CDATA[<p>La question à 2 centimes d&#8217;euros du jour : qu&#8217;est-ce qu&#8217;on obtient comme résultat avec une requête SQL de ce genre de celle ci-dessous (avec un MySQL) ?</p>
<pre>select * from ma_table where id = 42 = 0;</pre>
<p>La réponse est : toute la table sauf la ligne dont l&#8217;id vaut 42 ! En fait, en raison de la précédence des opérateurs, cette requête est équivalente à&nbsp;:</p>
<pre>select * from ma_table where (id = 42) = 0</pre>
<p>Comme l&#8217;opérateur de comparaison <code>=</code> <a href="http://dev.mysql.com/doc/refman/5.0/fr/comparison-operators.html">retourne 0 (FALSE) ou 1 (TRUE)</a> (ou NULL si un des 2 arguments vaut NULL), l&#8217;expression <code>id = 42 = 0</code> est parfaitement valide et est équivalente à <code>id != 42</code>.</p>
<p>Mais à quoi ça sert me direz-vous ? A faire des injections SQL dans un site PHP qui ne filtre pas correctement ses variables&#8230; A bon entendeur !</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cloudconnected.fr/2007/11/22/une-syntaxe-sql-originale/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Stocker un bool avec un enum, c&#8217;est mal.</title>
		<link>http://www.cloudconnected.fr/2007/10/25/stocker-un-bool-avec-un-enum-cest-mal/</link>
		<comments>http://www.cloudconnected.fr/2007/10/25/stocker-un-bool-avec-un-enum-cest-mal/#comments</comments>
		<pubDate>Thu, 25 Oct 2007 10:39:42 +0000</pubDate>
		<dc:creator>Rémi</dc:creator>
				<category><![CDATA[Non classé]]></category>
		<category><![CDATA[mysql]]></category>

		<guid isPermaLink="false">http://www.the-asw.com/?p=297</guid>
		<description><![CDATA[Avec MySQL, stocker un booléen (0 ou 1) dans un type enum (plutôt que dans un type numérique comme le ferait toute personne saine d&#8217;esprit), est une *très* mauvaise idée, qui fera à coup sûr perdre quelques heures au développeur qui devra maintenir le code. Et voici pourquoi. Disons que la table qui a été [...]]]></description>
			<content:encoded><![CDATA[<p>Avec MySQL, stocker un booléen (0 ou 1) dans un type <code>enum</code> (plutôt que dans un type numérique comme le ferait toute personne saine d&#8217;esprit), est une *très* mauvaise idée, qui fera à coup sûr perdre quelques heures au développeur qui devra maintenir le code. Et voici pourquoi.</p>
<p>Disons que la table qui a été créée ressemble à ça :</p>
<pre>
create table enum_is_evil (
  id integer not null auto_increment,
  value enum('0','1') not null default '0',
  primary key (id)
);

insert into enum_is_evil (value) values ('0'), ('0'), ('1');
</pre>
<p>Si on ne fait pas gaffe qu&#8217;il s&#8217;agit d&#8217;un enum et pas d&#8217;un type numérique (comme on pourrait logiquement le supposer), voila ce que ça donne&#8230;</p>
<pre>
mysql> select * from enum_is_evil where value = 0;
Empty set (0.00 sec)
</pre>
<p>Bon, aucun résultat alors qu&#8217;on devrait en avoir 2&#8230; Essayons autre chose.</p>
<pre>
mysql> select * from enum_is_evil where value = 1;
+----+-------+
| id | value |
+----+-------+
|  1 | 0     |
|  2 | 0     |
+----+-------+
2 rows in set (0.00 sec)
</pre>
<p>Youpi ! C&#8217;est n&#8217;importe quoi !</p>
<p>Et voyons ce qui se passe quand on ne fait pas attention à l&#8217;insertion, c&#8217;est encore plus drole.</p>
<pre>insert into enum_is_evil (value) values (0), (0), (1);</pre>
<p>Note : cette requête produit des warnings (logique), mais lorsqu&#8217;elle est executée dans un script, il n&#8217;y aucune chance de s&#8217;en apercevoir.</p>
<pre>
mysql> select * from enum_is_evil where value = 0;
+----+-------+
| id | value |
+----+-------+
|  4 |       |
|  5 |       |
+----+-------+
2 rows in set (0.00 sec)
</pre>
<p>Yeah, des valeurs vides dans la table alors qu&#8217;on devrait avoir uniquement des 0 ou des 1 !</p>
<pre>
mysql> select * from enum_is_evil where value = 1;
+----+-------+
| id | value |
+----+-------+
|  1 | 0     |
|  2 | 0     |
|  6 | 0     |
+----+-------+
3 rows in set (0.00 sec)
</pre>
<p>Encore mieux, la valeur &#8220;1&#8243; qu&#8217;on a ajoutée est devenue un &#8220;0&#8243; !</p>
<p>Conclusion : bien que tout ça soit logique quand on lit <a href="http://dev.mysql.com/doc/refman/5.0/fr/enum.html">la doc du type <code>ENUM</code></a> (il suffit d&#8217;ajouter des quotes autour des valeurs pour obtenir les bons résultats), c&#8217;est une source d&#8217;erreur potentielle assez vicieuse quand on s&#8217;attend à travailler avec un type numérique. Utilisez plutôt un <code>TINYINT</code> !</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cloudconnected.fr/2007/10/25/stocker-un-bool-avec-un-enum-cest-mal/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Requêtes pour faire du CSV avec MySQL</title>
		<link>http://www.cloudconnected.fr/2007/09/12/requetes-pour-faire-du-csv-avec-mysql/</link>
		<comments>http://www.cloudconnected.fr/2007/09/12/requetes-pour-faire-du-csv-avec-mysql/#comments</comments>
		<pubDate>Wed, 12 Sep 2007 07:42:01 +0000</pubDate>
		<dc:creator>Rémi</dc:creator>
				<category><![CDATA[Non classé]]></category>
		<category><![CDATA[csv]]></category>
		<category><![CDATA[mysql]]></category>

		<guid isPermaLink="false">http://www.the-asw.com/?p=295</guid>
		<description><![CDATA[Voici une petite requête pour MySQL &#8805; 5 qui peut être utile si vous cherchez à obtenir la liste des champs d&#8217;une table, pour l&#8217;exporter en tant qu&#8217;entête d&#8217;un fichier CSV par exemple. select group_concat(column_name SEPARATOR ";") from information_schema.columns where table_schema = 'DB_NAME' and table_name = 'TABLE_NAME' group by table_name; La requête suivant permet d&#8217;exporter [...]]]></description>
			<content:encoded><![CDATA[<p>Voici une petite requête pour MySQL &ge; 5 qui peut être utile si vous cherchez à obtenir la liste des champs d&#8217;une table, pour l&#8217;exporter en tant qu&#8217;entête d&#8217;un fichier CSV par exemple.</p>
<pre>
select group_concat(column_name SEPARATOR ";")
from information_schema.columns
where table_schema = '<em>DB_NAME</em>' and table_name = '<em>TABLE_NAME</em>'
group by table_name;
</pre>
<p>La requête suivant permet d&#8217;exporter une table complète en CSV, sans passer par un langage de script (à condition d&#8217;avoir <a href="http://dev.mysql.com/doc/refman/5.0/fr/privileges-provided.html">la permission <code>FILE</code></a>).</p>
<pre>
select *
into outfile '<em>/tmp/foo.csv</em>'
fields terminated by ';' optionally enclosed by '"'
lines terminated by '\n'
from <em>DB_NAME</em>.<em>TABLE_NAME</em>;
</pre>
<p>Plus d&#8217;infos dans <a href="http://dev.mysql.com/doc/refman/5.0/fr/select.html">la doc de MySQL</a>.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cloudconnected.fr/2007/09/12/requetes-pour-faire-du-csv-avec-mysql/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>

