如何在 PostgreSQL 中使用语言相关数据进行分层查询
How to hierarchical query in PostgreSQL with language-dependent data
我正在尝试从对自动引用 table 的查询中检索分层排序结果,如下所示:
create table category (
id serial,
-- parent category,
parent_id integer default null, -- null for root category
-- tree control
tree_depth smallint not null, -- calculated
primary key (id),
unique (parent_id, id),
foreign key (parent_id) references category (id)
);
除了需要支持多种语言外,这是存储类别树的常用方法。为此,我们加入了一个依赖于语言的 table,如下所示:
create table category_lang (
id serial,
-- natural primary key
category_id integer not null,
lang_code char(2) not null,
-- language-dependent data
title varchar(128) not null,
primary key (id),
unique (category_id, lang_code)
);
tree_depth
列在 before insert
触发器中计算如下:
create or replace function fn_category__bins () returns trigger as $$
begin
-- calculate tree_depth as parent tree_depth + 1
if new.parent_id is null then
new.tree_depth = 0;
else
new.tree_depth = (select tree_depth from category where id = new.parent_id limit 1) + 1;
end if;
return new;
end;
$$ language plpgsql;
create trigger tg_category__bins before insert on category for each row
execute procedure fn_category__bins();
我们用两种语言的易于阅读的文本填充 table:
insert into category (parent_id, id) values
(null, 1),
(null, 2),
(null, 3),
(1, 11),
(1, 12),
(1, 13),
(2, 21),
(2, 22),
(3, 31),
(21, 211),
(21, 212),
(21, 213);
-- lang_code = 'EN'
insert into category_lang (category_id, title, lang_code) values
(1, 'One', 'EN'),
(2, 'Two', 'EN'),
(3, 'Three', 'EN'),
(11, 'One.One', 'EN'),
(12, 'One.Two', 'EN'),
(13, 'One.Three', 'EN'),
(21, 'Two.One', 'EN'),
(22, 'Two.Two', 'EN'),
(31, 'Three.One', 'EN'),
(211, 'Two.One.One', 'EN'),
(212, 'Two.One.Two', 'EN'),
(213, 'Two.One.Three', 'EN');
-- lang_code = 'ES'
insert into category_lang (category_id, title, lang_code) values
(1, 'Uno', 'ES'),
(2, 'Dos', 'ES'),
(3, 'Tres', 'ES'),
(11, 'Uno.Uno', 'ES'),
(12, 'Uno.Dos', 'ES'),
(13, 'Uno.Tres', 'ES'),
(21, 'Dos.Uno', 'ES'),
(22, 'Dos.Dos', 'ES'),
(31, 'Tres.Uno', 'ES'),
(211, 'Dos.Uno.Uno', 'ES'),
(212, 'Dos.Uno.Dos', 'ES'),
(213, 'Dos.Uno.Tres', 'ES');
一个简单的查询会产生这样的自然结果:
select * from category tc
left outer join category_lang tl on tl.category_id = tc.id and tl.lang_code = 'EN';
id |parent_id|tree_depth|id|category_id|lang_code|title |
---|---------|----------|--|-----------|---------|-------------|
1| | 0| 1| 1|EN |One |
2| | 0| 2| 2|EN |Two |
3| | 0| 3| 3|EN |Three |
11| 1| 1| 4| 11|EN |One.One |
12| 1| 1| 5| 12|EN |One.Two |
13| 1| 1| 6| 13|EN |One.Three |
21| 2| 1| 7| 21|EN |Two.One |
22| 2| 1| 8| 22|EN |Two.Two |
31| 3| 1| 9| 31|EN |Three.One |
211| 21| 2|10| 211|EN |Two.One.One |
212| 21| 2|11| 212|EN |Two.One.Two |
213| 21| 2|12| 213|EN |Two.One.Three|
当预期顺序应符合树状层次结构和英文字母顺序(在每个深度级别)时,如下所示:
[编辑以修复 Erwin 发现的错误]
id |parent_id|tree_depth|id|category_id|lang_code|title |
---|---------|----------|--|-----------|---------|-------------|
1| | 0| 1| 1|EN |One |
11| 1| 1| 4| 11|EN |One.One |
13| 1| 1| 6| 13|EN |One.Three |
12| 1| 1| 5| 12|EN |One.Two |
3| | 0| 3| 3|EN |Three |
31| 3| 1| 9| 31|EN |Three.One |
2| | 0| 2| 2|EN |Two |
21| 2| 1| 7| 21|EN |Two.One |
211| 21| 2|10| 211|EN |Two.One.One |
213| 21| 2|12| 213|EN |Two.One.Three|
212| 21| 2|11| 212|EN |Two.One.Two |
22| 2| 1| 8| 22|EN |Two.Two |
请注意,每个深度的字母顺序都会对西班牙语产生不同的结果:
[编辑以修复 Erwin 发现的错误]
id |parent_id|tree_depth|id|category_id|lang_code|title |
---|---------|----------|--|-----------|---------|------------|
2| | 0|14| 2|ES |Dos |
22| 2| 1|20| 22|ES |Dos.Dos |
21| 2| 1|19| 21|ES |Dos.Uno |
212| 21| 2|23| 212|ES |Dos.Uno.Dos |
213| 21| 2|24| 213|ES |Dos.Uno.Tres|
211| 21| 2|22| 211|ES |Dos.Uno.Uno |
1| | 0|13| 1|ES |Uno |
12| 1| 1|17| 12|ES |Uno.Dos |
13| 1| 1|18| 13|ES |Uno.Tres |
11| 1| 1|16| 11|ES |Uno.Uno |
3| | 0|15| 3|ES |Tres |
31| 3| 1|21| 31|ES |Tres.Uno |
我尝试了很多方法,包括 https://www.postgresql.org/docs/12/queries-with.html 中的递归 CTE,但 none 似乎可以解决不同语言的不同顺序问题。
有什么想法吗?
... the expected order should be compliant with tree hierarchy and alphabetical order in English (at every depth level),
额外的困难是category_lang(title, lang_code)
没有定义UNIQUE
,所以我们需要按title
和[=16=排序](作为决胜局)在每个级别上 - 这对于动态数量的级别很难实现。 复合类型的数组可以解决这个难题。
您显示的结果目前不符合您的要求。根据英文排序规则,'Three' 应该排在 'Two' 之前。以下查询的结果实现了您的要求:
为每个数据库创建一次:
CREATE TYPE title_id AS (title varchar(128), id int);
然后用一个递归的CTE根据路径生成这个复合类型的数组
WITH RECURSIVE tree AS (
SELECT c.id AS cat_id, c.parent_id, c.tree_depth
, l.id AS lang_id, l.title, l.lang_code
, ARRAY[(l.title, l.category_id)::title_id] AS sort_arr
FROM category c
JOIN category_lang l ON l.category_id = c.id
AND l.lang_code = 'EN'
WHERE c.parent_id IS NULL -- root cat
UNION ALL
SELECT c.id AS cat_id, c.parent_id, c.tree_depth
, l.id AS lang_id, l.title, l.lang_code
, sort_arr || (l.title, l.category_id)::title_id
FROM tree t
JOIN category c ON c.parent_id = t.cat_id
JOIN category_lang l ON l.category_id = c.id
AND l.lang_code = t.lang_code
)
SELECT cat_id, parent_id, tree_depth, lang_id, title
FROM tree
ORDER BY sort_arr;
db<>fiddle here
与更多解释和细节密切相关:
- Retrieving full hierarchy sorted by a column under PostgreSQL's Ltree module
COLLATE
?
但这还不是全部。简单的解决方案按数据库的默认排序规则排序,这似乎不适合不同的语言。
每种语言都有自己的 排序规则 规则,或者通常有多个规则,具体取决于世界区域和其他政治/文化规则。 “语言”不足以指定准确的排序规则。精确的 locale 很重要。 Postgres 使用 COLLATE
关键字实现排序规则感知排序。除了语言之外,您还必须存储实际的精确排序规则,并使用它来正确排序。
此外,索引取决于确切的 COLLATION
。您可能会考虑使用不同排序规则的多个部分索引。许多棘手的事情超出了这个问题的范围。参见:
- How to choose a collation for international database?
- PostgreSQL 9.1 using collate in select statements
旁白
您的触发器在并发写入之间的竞争条件下不安全。出于此查询的目的,我们根本不需要持久化的 tree_depth
。我们可以在 rCTE 中轻松生成它。考虑删除列 tree_depth
和触发器。
table中似乎缺少 FK 约束 category_lang
:
, FOREIGN KEY (category_id) REFERENCES category (id)
考虑 text
而不是 varchar(n)
和 char(n)
。参见:
- Any downsides of using data type "text" for storing strings?
- Should I add an arbitrary length limit to VARCHAR columns?
考虑 IDENTITY
列而不是 serial
:
- Auto increment table column
在 Erwin 用更简单的解决方案回答之前,我自己做了这个递归方法。插入一个虚拟 root
类别效果更好,它允许我们从单个入口点检索整个树。
create or replace function list_category_tree (
_category_id integer,
_lang_code char(2)
)
returns setof category
as $$
declare
_child_category category;
begin
-- return the passed category
return query
select * from category where id = _category_id;
-- loop over the passed category children
for _child_category in
select tc.*
from category tc
join category_lang tl on tl.category_id = tc.id and tl.lang_code = _lang_code
where tc.parent_id = _category_id
order by tl.title asc
loop
-- recursively look for every children childrens
return query
select * from list_category_tree(_child_category.id, _lang_code);
end loop;
end;
$$ language plpgsql;
一个简单的测试可以是
select *
from list_category_tree (0, 'EN') as tc
join category_lang tl on tl.category_id = tc.id and tl.lang_code = 'EN';
id |parent_id|tree_depth|id|category_id|lang_code|title |
---|---------|----------|--|-----------|---------|-------------|
1| | 0| 1| 1|EN |One |
11| 1| 1| 4| 11|EN |One.One |
13| 1| 1| 6| 13|EN |One.Three |
12| 1| 1| 5| 12|EN |One.Two |
2| | 0| 2| 2|EN |Two |
21| 2| 1| 7| 21|EN |Two.One |
211| 21| 2|10| 211|EN |Two.One.One |
213| 21| 2|12| 213|EN |Two.One.Three|
212| 21| 2|11| 212|EN |Two.One.Two |
22| 2| 1| 8| 22|EN |Two.Two |
3| | 0| 3| 3|EN |Three |
31| 3| 1| 9| 31|EN |Three.One |
select *
from list_category_tree (0, 'ES') as tc
join of_category_lang tl on tl.category_id = tc.id and tl.lang_code = 'ES';
id |parent_id|tree_depth|id|category_id|lang_code|title |
---|---------|----------|--|-----------|---------|------------|
2| 0| 1|14| 2|ES |Dos |
22| 2| 2|20| 22|ES |Dos.Dos |
21| 2| 2|19| 21|ES |Dos.Uno |
212| 21| 3|23| 212|ES |Dos.Uno.Dos |
213| 21| 3|24| 213|ES |Dos.Uno.Tres|
211| 21| 3|22| 211|ES |Dos.Uno.Uno |
3| 0| 1|15| 3|ES |Tres |
31| 3| 2|21| 31|ES |Tres.Uno |
1| 0| 1|13| 1|ES |Uno |
12| 1| 2|17| 12|ES |Uno.Dos |
13| 1| 2|18| 13|ES |Uno.Tres |
11| 1| 2|16| 11|ES |Uno.Uno |
已将根节点插入为
insert into of_category (parent_id, id) values
(null, 0),
(null, 1),
(null, 2),
(null, 3),
(1, 11),
(1, 12),
(1, 13),
(2, 21),
(2, 22),
(3, 31),
(21, 211),
(21, 212),
(21, 213);
-- lang_code = 'EN'
insert into of_category_lang (category_id, title, lang_code) values
(0, 'Root', 'EN'),
(1, 'One', 'EN'),
(2, 'Two', 'EN'),
(3, 'Three', 'EN'),
(11, 'One.One', 'EN'),
(12, 'One.Two', 'EN'),
(13, 'One.Three', 'EN'),
(21, 'Two.One', 'EN'),
(22, 'Two.Two', 'EN'),
(31, 'Three.One', 'EN'),
(211, 'Two.One.One', 'EN'),
(212, 'Two.One.Two', 'EN'),
(213, 'Two.One.Three', 'EN');
-- lang_code = 'ES'
insert into of_category_lang (category_id, title, lang_code) values
(0, 'Raíz', 'ES'),
(1, 'Uno', 'ES'),
(2, 'Dos', 'ES'),
(3, 'Tres', 'ES'),
(11, 'Uno.Uno', 'ES'),
(12, 'Uno.Dos', 'ES'),
(13, 'Uno.Tres', 'ES'),
(21, 'Dos.Uno', 'ES'),
(22, 'Dos.Dos', 'ES'),
(31, 'Tres.Uno', 'ES'),
(211, 'Dos.Uno.Uno', 'ES'),
(212, 'Dos.Uno.Dos', 'ES'),
(213, 'Dos.Uno.Tres', 'ES');
我正在尝试从对自动引用 table 的查询中检索分层排序结果,如下所示:
create table category (
id serial,
-- parent category,
parent_id integer default null, -- null for root category
-- tree control
tree_depth smallint not null, -- calculated
primary key (id),
unique (parent_id, id),
foreign key (parent_id) references category (id)
);
除了需要支持多种语言外,这是存储类别树的常用方法。为此,我们加入了一个依赖于语言的 table,如下所示:
create table category_lang (
id serial,
-- natural primary key
category_id integer not null,
lang_code char(2) not null,
-- language-dependent data
title varchar(128) not null,
primary key (id),
unique (category_id, lang_code)
);
tree_depth
列在 before insert
触发器中计算如下:
create or replace function fn_category__bins () returns trigger as $$
begin
-- calculate tree_depth as parent tree_depth + 1
if new.parent_id is null then
new.tree_depth = 0;
else
new.tree_depth = (select tree_depth from category where id = new.parent_id limit 1) + 1;
end if;
return new;
end;
$$ language plpgsql;
create trigger tg_category__bins before insert on category for each row
execute procedure fn_category__bins();
我们用两种语言的易于阅读的文本填充 table:
insert into category (parent_id, id) values
(null, 1),
(null, 2),
(null, 3),
(1, 11),
(1, 12),
(1, 13),
(2, 21),
(2, 22),
(3, 31),
(21, 211),
(21, 212),
(21, 213);
-- lang_code = 'EN'
insert into category_lang (category_id, title, lang_code) values
(1, 'One', 'EN'),
(2, 'Two', 'EN'),
(3, 'Three', 'EN'),
(11, 'One.One', 'EN'),
(12, 'One.Two', 'EN'),
(13, 'One.Three', 'EN'),
(21, 'Two.One', 'EN'),
(22, 'Two.Two', 'EN'),
(31, 'Three.One', 'EN'),
(211, 'Two.One.One', 'EN'),
(212, 'Two.One.Two', 'EN'),
(213, 'Two.One.Three', 'EN');
-- lang_code = 'ES'
insert into category_lang (category_id, title, lang_code) values
(1, 'Uno', 'ES'),
(2, 'Dos', 'ES'),
(3, 'Tres', 'ES'),
(11, 'Uno.Uno', 'ES'),
(12, 'Uno.Dos', 'ES'),
(13, 'Uno.Tres', 'ES'),
(21, 'Dos.Uno', 'ES'),
(22, 'Dos.Dos', 'ES'),
(31, 'Tres.Uno', 'ES'),
(211, 'Dos.Uno.Uno', 'ES'),
(212, 'Dos.Uno.Dos', 'ES'),
(213, 'Dos.Uno.Tres', 'ES');
一个简单的查询会产生这样的自然结果:
select * from category tc
left outer join category_lang tl on tl.category_id = tc.id and tl.lang_code = 'EN';
id |parent_id|tree_depth|id|category_id|lang_code|title |
---|---------|----------|--|-----------|---------|-------------|
1| | 0| 1| 1|EN |One |
2| | 0| 2| 2|EN |Two |
3| | 0| 3| 3|EN |Three |
11| 1| 1| 4| 11|EN |One.One |
12| 1| 1| 5| 12|EN |One.Two |
13| 1| 1| 6| 13|EN |One.Three |
21| 2| 1| 7| 21|EN |Two.One |
22| 2| 1| 8| 22|EN |Two.Two |
31| 3| 1| 9| 31|EN |Three.One |
211| 21| 2|10| 211|EN |Two.One.One |
212| 21| 2|11| 212|EN |Two.One.Two |
213| 21| 2|12| 213|EN |Two.One.Three|
当预期顺序应符合树状层次结构和英文字母顺序(在每个深度级别)时,如下所示:
[编辑以修复 Erwin 发现的错误]
id |parent_id|tree_depth|id|category_id|lang_code|title |
---|---------|----------|--|-----------|---------|-------------|
1| | 0| 1| 1|EN |One |
11| 1| 1| 4| 11|EN |One.One |
13| 1| 1| 6| 13|EN |One.Three |
12| 1| 1| 5| 12|EN |One.Two |
3| | 0| 3| 3|EN |Three |
31| 3| 1| 9| 31|EN |Three.One |
2| | 0| 2| 2|EN |Two |
21| 2| 1| 7| 21|EN |Two.One |
211| 21| 2|10| 211|EN |Two.One.One |
213| 21| 2|12| 213|EN |Two.One.Three|
212| 21| 2|11| 212|EN |Two.One.Two |
22| 2| 1| 8| 22|EN |Two.Two |
请注意,每个深度的字母顺序都会对西班牙语产生不同的结果:
[编辑以修复 Erwin 发现的错误]
id |parent_id|tree_depth|id|category_id|lang_code|title |
---|---------|----------|--|-----------|---------|------------|
2| | 0|14| 2|ES |Dos |
22| 2| 1|20| 22|ES |Dos.Dos |
21| 2| 1|19| 21|ES |Dos.Uno |
212| 21| 2|23| 212|ES |Dos.Uno.Dos |
213| 21| 2|24| 213|ES |Dos.Uno.Tres|
211| 21| 2|22| 211|ES |Dos.Uno.Uno |
1| | 0|13| 1|ES |Uno |
12| 1| 1|17| 12|ES |Uno.Dos |
13| 1| 1|18| 13|ES |Uno.Tres |
11| 1| 1|16| 11|ES |Uno.Uno |
3| | 0|15| 3|ES |Tres |
31| 3| 1|21| 31|ES |Tres.Uno |
我尝试了很多方法,包括 https://www.postgresql.org/docs/12/queries-with.html 中的递归 CTE,但 none 似乎可以解决不同语言的不同顺序问题。
有什么想法吗?
... the expected order should be compliant with tree hierarchy and alphabetical order in English (at every depth level),
额外的困难是category_lang(title, lang_code)
没有定义UNIQUE
,所以我们需要按title
和[=16=排序](作为决胜局)在每个级别上 - 这对于动态数量的级别很难实现。 复合类型的数组可以解决这个难题。
您显示的结果目前不符合您的要求。根据英文排序规则,'Three' 应该排在 'Two' 之前。以下查询的结果实现了您的要求:
为每个数据库创建一次:
CREATE TYPE title_id AS (title varchar(128), id int);
然后用一个递归的CTE根据路径生成这个复合类型的数组
WITH RECURSIVE tree AS (
SELECT c.id AS cat_id, c.parent_id, c.tree_depth
, l.id AS lang_id, l.title, l.lang_code
, ARRAY[(l.title, l.category_id)::title_id] AS sort_arr
FROM category c
JOIN category_lang l ON l.category_id = c.id
AND l.lang_code = 'EN'
WHERE c.parent_id IS NULL -- root cat
UNION ALL
SELECT c.id AS cat_id, c.parent_id, c.tree_depth
, l.id AS lang_id, l.title, l.lang_code
, sort_arr || (l.title, l.category_id)::title_id
FROM tree t
JOIN category c ON c.parent_id = t.cat_id
JOIN category_lang l ON l.category_id = c.id
AND l.lang_code = t.lang_code
)
SELECT cat_id, parent_id, tree_depth, lang_id, title
FROM tree
ORDER BY sort_arr;
db<>fiddle here
与更多解释和细节密切相关:
- Retrieving full hierarchy sorted by a column under PostgreSQL's Ltree module
COLLATE
?
但这还不是全部。简单的解决方案按数据库的默认排序规则排序,这似乎不适合不同的语言。
每种语言都有自己的 排序规则 规则,或者通常有多个规则,具体取决于世界区域和其他政治/文化规则。 “语言”不足以指定准确的排序规则。精确的 locale 很重要。 Postgres 使用 COLLATE
关键字实现排序规则感知排序。除了语言之外,您还必须存储实际的精确排序规则,并使用它来正确排序。
此外,索引取决于确切的 COLLATION
。您可能会考虑使用不同排序规则的多个部分索引。许多棘手的事情超出了这个问题的范围。参见:
- How to choose a collation for international database?
- PostgreSQL 9.1 using collate in select statements
旁白
您的触发器在并发写入之间的竞争条件下不安全。出于此查询的目的,我们根本不需要持久化的
tree_depth
。我们可以在 rCTE 中轻松生成它。考虑删除列tree_depth
和触发器。table中似乎缺少 FK 约束
category_lang
:, FOREIGN KEY (category_id) REFERENCES category (id)
考虑
text
而不是varchar(n)
和char(n)
。参见:- Any downsides of using data type "text" for storing strings?
- Should I add an arbitrary length limit to VARCHAR columns?
考虑
IDENTITY
列而不是serial
:- Auto increment table column
在 Erwin 用更简单的解决方案回答之前,我自己做了这个递归方法。插入一个虚拟 root
类别效果更好,它允许我们从单个入口点检索整个树。
create or replace function list_category_tree (
_category_id integer,
_lang_code char(2)
)
returns setof category
as $$
declare
_child_category category;
begin
-- return the passed category
return query
select * from category where id = _category_id;
-- loop over the passed category children
for _child_category in
select tc.*
from category tc
join category_lang tl on tl.category_id = tc.id and tl.lang_code = _lang_code
where tc.parent_id = _category_id
order by tl.title asc
loop
-- recursively look for every children childrens
return query
select * from list_category_tree(_child_category.id, _lang_code);
end loop;
end;
$$ language plpgsql;
一个简单的测试可以是
select *
from list_category_tree (0, 'EN') as tc
join category_lang tl on tl.category_id = tc.id and tl.lang_code = 'EN';
id |parent_id|tree_depth|id|category_id|lang_code|title |
---|---------|----------|--|-----------|---------|-------------|
1| | 0| 1| 1|EN |One |
11| 1| 1| 4| 11|EN |One.One |
13| 1| 1| 6| 13|EN |One.Three |
12| 1| 1| 5| 12|EN |One.Two |
2| | 0| 2| 2|EN |Two |
21| 2| 1| 7| 21|EN |Two.One |
211| 21| 2|10| 211|EN |Two.One.One |
213| 21| 2|12| 213|EN |Two.One.Three|
212| 21| 2|11| 212|EN |Two.One.Two |
22| 2| 1| 8| 22|EN |Two.Two |
3| | 0| 3| 3|EN |Three |
31| 3| 1| 9| 31|EN |Three.One |
select *
from list_category_tree (0, 'ES') as tc
join of_category_lang tl on tl.category_id = tc.id and tl.lang_code = 'ES';
id |parent_id|tree_depth|id|category_id|lang_code|title |
---|---------|----------|--|-----------|---------|------------|
2| 0| 1|14| 2|ES |Dos |
22| 2| 2|20| 22|ES |Dos.Dos |
21| 2| 2|19| 21|ES |Dos.Uno |
212| 21| 3|23| 212|ES |Dos.Uno.Dos |
213| 21| 3|24| 213|ES |Dos.Uno.Tres|
211| 21| 3|22| 211|ES |Dos.Uno.Uno |
3| 0| 1|15| 3|ES |Tres |
31| 3| 2|21| 31|ES |Tres.Uno |
1| 0| 1|13| 1|ES |Uno |
12| 1| 2|17| 12|ES |Uno.Dos |
13| 1| 2|18| 13|ES |Uno.Tres |
11| 1| 2|16| 11|ES |Uno.Uno |
已将根节点插入为
insert into of_category (parent_id, id) values
(null, 0),
(null, 1),
(null, 2),
(null, 3),
(1, 11),
(1, 12),
(1, 13),
(2, 21),
(2, 22),
(3, 31),
(21, 211),
(21, 212),
(21, 213);
-- lang_code = 'EN'
insert into of_category_lang (category_id, title, lang_code) values
(0, 'Root', 'EN'),
(1, 'One', 'EN'),
(2, 'Two', 'EN'),
(3, 'Three', 'EN'),
(11, 'One.One', 'EN'),
(12, 'One.Two', 'EN'),
(13, 'One.Three', 'EN'),
(21, 'Two.One', 'EN'),
(22, 'Two.Two', 'EN'),
(31, 'Three.One', 'EN'),
(211, 'Two.One.One', 'EN'),
(212, 'Two.One.Two', 'EN'),
(213, 'Two.One.Three', 'EN');
-- lang_code = 'ES'
insert into of_category_lang (category_id, title, lang_code) values
(0, 'Raíz', 'ES'),
(1, 'Uno', 'ES'),
(2, 'Dos', 'ES'),
(3, 'Tres', 'ES'),
(11, 'Uno.Uno', 'ES'),
(12, 'Uno.Dos', 'ES'),
(13, 'Uno.Tres', 'ES'),
(21, 'Dos.Uno', 'ES'),
(22, 'Dos.Dos', 'ES'),
(31, 'Tres.Uno', 'ES'),
(211, 'Dos.Uno.Uno', 'ES'),
(212, 'Dos.Uno.Dos', 'ES'),
(213, 'Dos.Uno.Tres', 'ES');