标签:doc main 文件 理解 def phone gem 加ip 哪些
概念CMDB: Configuration Management DB,配置管理数据库。它管理的是资产,核心是DB
,即存放资产信息的系统,而在实际中,运维团队会围绕这个DB
衍生出运维管理系统,该系统依赖CMDB
中的基础信息。
数据库表设计
资产类型表
id
name
如: 1 服务器, 2 交换机, 3 路由器
资产表
id
name
资产类型id
设备编号
如: 1 dell服务器 1 d1002933, 2 H3C路由器 3 r234000033
路由器表
id
配置信息
机架信息
责任人
...
由于资产的多样性,这样设计表,如果增加一个资产,那就需要创建表,如果一个已有的资产需要增加管理信息,那就需要增加字段,反之就需要删除字段或删除表,当然生产环境中当然不建议数据的真删除。
这种传统的设计,资产的多样性只能以增加表的方式来表达。
需要充分考虑运维日常管理信息的复杂性,将管理信息所需要的表、字段、值抽离出来形成不同的表。
做模型设计时工具有许多,这里使用的是MySQLWorkbench,在使用MySQLWorkbench进行模型设计时一定要把目标mysql的版本设置成自己使用数据库的版本,因高版本有些语句不兼容低版本。设置方式:在Preference --> Modeing --> MySQL 在model一栏的 Default Target MySQL Version 中设置成对应的版本,如设置成“5.7”。
表设计
schema
表: 描述有哪些表,一行数据表示一个具体的资产表的名称,相应的sql如下:
CREATE TABLE IF NOT EXISTS `cmdb`.`schema` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(48) NOT NULL,
`desc` VARCHAR(128) NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `name_UNIQUE` (`name` ASC))
ENGINE = InnoDB
field
表: 描述一个表的字段,一行数据表示归属一个特定资产表中的字段;该表的多条数据可以对应schema表中的一条数据,即对应一个特定的资产表,这里field表与schema表就形成一个外键约束,field表以多对一
的关系对应schema表,相应的sql如下:
CREATE TABLE IF NOT EXISTS `cmdb`.`field` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(48) NOT NULL,
`meta` TEXT NULL,
`schema_id` INT NOT NULL,
PRIMARY KEY (`id`),
INDEX `fk_field_schema1_idx` (`schema_id` ASC),
CONSTRAINT `fk_field_schema1`
FOREIGN KEY (`schema_id`)
REFERENCES `cmdb`.`schema` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB
schema
与field
两个表结合起来就可描述资产管理系统的对应各种资产的表,如:
schema表数据
id | name | Desc |
---|---|---|
1 | host | 主机表 |
2 | route | 路由器表 |
field表数据
id | name | meta | schema_id |
---|---|---|---|
1 | name | 1 | |
2 | ip | 1 | |
3 | rack_number | 1 | |
4 | ip | 2 | |
5 | rack_number | 2 |
field表中的meta
字段先不用管它,它是后边描述字段的元数据信息的,这个后面会用到。如上两个表中的数据就创建两个资产表,一个名为host
,另一个为route
,组合起来就可以描述表和相应的字段。上边表示host表有三个字段,为name
、ip
r和rack_number
,route表有2个字段,为ip
和rackk_number
。这种host和route表我们叫做逻辑表
。
这样设计后,如果要增加资产,在schema表中增加一条记录,并在field表中增加相应的字段即可,向表中增加一条记录,对mysql来说相当方便。那逻辑表
中的一行行记录怎样存放呢,在filed表中存放是不合适的,在schema表更不合适,所以需要再增加表才行。
再增加一张表,表名为entity
,该表记录有一条资产记录,此表需要描述该记录是对应哪个逻辑表
,即entity
表与schema
表也是多对一的关系。
再增加一张表,表名为value
,此表才真正记录资产信息的数据,一行数据需要描述该数据属于哪个逻辑表
的哪个字段,所以value
表与entity
表也是多对一的关系,因为一个entity
表中的id对对应schema
表中的唯一一行数据,所以value
表中的多条数据都能对应entity
表中的一个id;value
表的多行数据也需要对应field
表的id,也是多对一的关系。最后entity
和value
表的sql语句如下:
entity
表:描述一行资产记录需要插入
CREATE TABLE IF NOT EXISTS `cmdb`.`entity` (
`id` INT NOT NULL AUTO_INCREMENT,
`key` VARCHAR(48) NOT NULL,
`schema_id` INT NOT NULL,
PRIMARY KEY (`id`),
INDEX `fk_entity_schema2_idx` (`schema_id` ASC),
CONSTRAINT `fk_entity_schema2`
FOREIGN KEY (`schema_id`)
REFERENCES `cmdb`.`schema` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB
entity表中的key
字段是记录一条记录的唯一性,一个uuid的值。
value
表:描述具体的资产信息属于哪张逻辑表
的哪个字段
CREATE TABLE IF NOT EXISTS `cmdb`.`value` (
`id` INT NOT NULL AUTO_INCREMENT,
`value` TEXT NOT NULL,
`field_id` INT NOT NULL,
`entity_id` INT NOT NULL,
PRIMARY KEY (`id`),
INDEX `fk_value_field2_idx` (`field_id` ASC),
INDEX `fk_value_entity2_idx` (`entity_id` ASC),
CONSTRAINT `fk_value_field2`
FOREIGN KEY (`field_id`)
REFERENCES `cmdb`.`field` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_value_entity2`
FOREIGN KEY (`entity_id`)
REFERENCES `cmdb`.`entity` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB
现在模拟向host这个逻辑表中插入一条数据,那entity
表就有一条类似这样的记录
id | key | schema_id |
---|---|---|
1 | xxxxxxxxxxxuuid | 1 |
value
表中的记录
id | value | filed_id | entity_id |
---|---|---|---|
1 | webserver01 | 1 | 1 |
2 | 192.168.1.20 | 2 | 1 |
3 | 001 | 3 | 1 |
这样一个资产记录主被记录好了。在MySQLWorkbench呈现如下:
为什么value
表中的value
字段的类型为TEXT?首先要明白value
字段是需要存放所有资产类型的数据,可能是一个ip,一个数字,一个字符串,布尔值,一个时间等,所以为TEXT类型是最为合适的。
数据库模型创建好后,就可以应用到MySQL中生成相应的表。现在来创建资产表,并向资产表中插入一些数据。
创建一张逻辑表,名为host
,并为该表创建两个字段,hostname
和ip
mysql> INSERT INTO `cmdb`.`schema` (`name`,`desc`) VALUES (‘host‘,‘host table‘);
mysql> INSERT INTO field (name, schema_id) values (‘hostname‘, 1);
mysql> INSERT INTO field (name, schema_id) values (‘ip‘, 1);
查询逻辑表
mysql> SELECT * FROM `schema`, field WHERE field.schema_id = `schema`.id and `schema`.id = 1;
+----+------+------------+----+----------+------+-----------+
| id | name | desc | id | name | meta | schema_id |
+----+------+------------+----+----------+------+-----------+
| 1 | host | host table | 1 | hostname | NULL | 1 |
| 1 | host | host table | 2 | ip | NULL | 1 |
+----+------+------------+----+----------+------+-----------+
2 rows in set (0.01 sec)
逻辑表有了,现在准备插入一条资产记录
mysql> INSERT INTO entity (`key`, schema_id) values (‘uuid00000001‘, 1);
mysql> SELECT * from entity;
+----+--------------+-----------+
| id | key | schema_id |
+----+--------------+-----------+
| 1 | uuid00000001 | 1 |
+----+--------------+-----------+
表示向将要向schema_id
为1
的逻辑表插入一条记录,即schema.id
为1
对应的逻辑表,就是host
逻辑表。现在来真正插入记录,如下:
mysql> INSERT INTO `value`(entity_id, field_id, `value`) values (1, 1, ‘webserver01‘);
Query OK, 1 row affected (0.01 sec)
mysql> INSERT INTO `value`(entity_id, field_id, `value`) values (1, 2, ‘192.168.0.1‘);
Query OK, 1 row affected (0.00 sec)
插入了一个名为webserver01
的主机,其IP地址为192.168.0.1
,再插入一条记录,如下:
mysql> INSERT INTO entity (`key`, schema_id) values (‘uuid00000002‘, 1);
Query OK, 1 row affected (0.01 sec)
mysql> SELECT * from entity;
+----+--------------+-----------+
| id | key | schema_id |
+----+--------------+-----------+
| 1 | uuid00000001 | 1 |
| 2 | uuid00000002 | 1 |
+----+--------------+-----------+
2 rows in set (0.00 sec)
mysql> INSERT INTO `value`(entity_id, field_id, `value`) values (2, 1, ‘webserver02‘);
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO `value`(entity_id, field_id, `value`) values (2, 2, ‘192.168.0.2‘);
Query OK, 1 row affected (0.00 sec)
mysql> select * from value;
+----+-------------+----------+-----------+
| id | value | field_id | entity_id |
+----+-------------+----------+-----------+
| 1 | webserver01 | 1 | 1 |
| 2 | 192.168.0.1 | 2 | 1 |
| 3 | webserver02 | 1 | 2 |
| 4 | 192.168.0.2 | 2 | 2 |
+----+-------------+----------+-----------+
那如何查询逻辑表
中的数据呢?
mysql> SELECT
entity.id AS entity_id, entity.`key`, entity.schema_id, `schema`.`name`, field.id AS fied_id, field.`name` AS fname, `value`.`value`
FROM
entity
INNER JOIN `value` ON entity.id = `value`.entity_id
INNER JOIN field ON field.id = `value`.field_id
INNER JOIN `schema` ON field.schema_id = `schema`.id
ORDER BY entity_id;
+-----------+--------------+-----------+------+---------+----------+-------------+
| entity_id | key | schema_id | name | fied_id | fname | value |
+-----------+--------------+-----------+------+---------+----------+-------------+
| 1 | uuid00000001 | 1 | host | 1 | hostname | webserver01 |
| 1 | uuid00000001 | 1 | host | 2 | ip | 192.168.0.1 |
| 2 | uuid00000002 | 1 | host | 1 | hostname | webserver02 |
| 2 | uuid00000002 | 1 | host | 2 | ip | 192.168.0.2 |
+-----------+--------------+-----------+------+---------+----------+-------------+
这样就能查询到逻辑表
对应的字段及字段对应的值。
这样设计表关系的好处:
不管有多少资产需要管理,只需在schema表中增加,不管资产有多少属性字段,只需要在field表中增加。
坏处:
表结构复杂了,关系复杂了,原来在一张表里能呈现的信息现在需要多张表来关联呈现。复杂的同时,带了灵活性。
如果要想描述逻辑表
中字段的一些约束,应该怎样来描述?比如host
逻辑中的ip
字段是唯一键约束,在value
表中的value
字段加上一个唯一键约束,这样合适吗?这样做不合适,因为value
这个字段是记录所有逻辑表
的字段值的,如果加上唯一键约束,那会影响所有的逻辑表
。
先交代下我的开发环境,python3.6
, django2.2
,下边的cmdb
为项目名称,dbapi
是创建的app。
(cmdbdev) MyMac:cmdb_django Admin$ tree .
.
├── cmdb
│?? ├── __init__.py
│?? ├── settings.py
│?? ├── urls.py
│?? └── wsgi.py
├── dbapi
│?? ├── __init__.py
│?? ├── admin.py
│?? ├── apps.py
│?? ├── migrations
│?? │?? ├── 0001_initial.py
│?? │?? ├── 0002_auto_20200901_1502.py
│?? │?? ├── 0003_auto_20200902_0135.py
│?? │?? ├── 0004_auto_20200902_0351.py
│?? │?? └── __init__.py
│?? ├── models.py
│?? ├── tests.py
│?? └── views.py
├── manage.py
└── test.py
4 directories, 18 files
在向value
表中插入数据时,相同的数据可以重复插入,例如“INSERT INTO value
(entity_id, field_id, value
) values (2, 1, ‘webserver02‘);”这条语句,可以重复执行多次,如果数据被重复插入,那在检索时就会出现重复的数据。这个问题可以在value
表上创建以field_id
和entity_id
两个字段联合的唯一键约束即可解决这个问题。
逻辑表
中字段如果有约束应该怎样定义呢,这里有得使用field
表的meta
字段,这个字段的类型为TEXT。此字段存放json格式的字符串,描述了该字段的一些属性信息,比如,此字段存放的是一个整形的数字,存放的是一个IP地址等。例如一个meta
信息如下,注意:value
这个字段并不会在meta
中出现,这是value
表中value
字段的值,放在这里只是为了更好的说明问题。
{
"type": "dbapi.types.Int",
"value": 100
}
或者
{
"type": "dbapi.types.IP",
"value": "192.168.1.23"
}
其中的type
的值是能对应到相应的类对象
,以一个字符串来动态加载一个类,这是利用了反射的特性,有了类对象就可实例化后调用相应的函数对value
进行相应的数据较验证。
存放数据的多样性,对各个字段的约束也会不尽相同,后期可能会编写许多dbapi.types
这个模块中的相应方法。现在先创建一个包dbapi/types
,在dbapi/types/__init__.py
文件中实现Int
类、IP
类等,如下:
import importlib
import ipaddress
def get_instance(meta_type: str):
# 获取较验类的实例对象
m, c = meta_type.rsplit(‘.‘, maxsplit=1)
mod = importlib.import_module(m) # 动态加载模块
cls = getattr(mod, c) # 反射,获取c对象
obj = cls()
if isinstance(obj, BaseType):
return obj
raise TypeError(‘{} is not BaseType‘.format(obj))
class BaseType:
def stringify(self, value):
# 从用户端拿到数据,转换成字符串,基类未实现
raise NotImplementedError()
def destringify(self, value):
# 从数据库拿数据,返回。还原数据的过程,基类不实现
raise NotImplementedError()
class Int(BaseType):
# 整形类型较验
def stringify(self, value):
return str(int(value)) # 最后要存放在value表的value字段里,所以为str
def destringify(self, value):
return value
class IP(BaseType):
# 检验IP地址
def stringify(self, value):
return str(ipaddress.ip_address(value)) # 如果value是一个合法的ip地址字符器,那就能被ipaddress操作
def destringify(self, value):
return value
在项目根目录下的test.py
文件中可以进行简单的测试
import json
from dbapi.types import get_instance
meta_str = """
{
"type":"dbapi.types.Int",
"value": "100"
}
"""
obj = json.loads(meta_str)
a = get_instance(obj[‘type‘]).stringify(obj[‘value‘])
print(a, type(a))
对IP地址的较验也类似,尝试修改meta_str中的value
的值修改为一个不合法的,一运行就会抛出相应的异常。
使用json格式的字符串来描述字段的约束关系时,用户需要与后端协商好type
是如何定义的,后端代码使用到的动态加载模块以及反射的使用需要好好体会。
如果value
表的value
字段为整形,且需要对这个字段做大小的约束,那meta
的json字符串可以定义如下:
{
"type":"dbapi.types.Int",
"option": {
"min": 10,
"max": 100
},
"value": "100"
}
后端代码只要拿到option
就可以做相应的判断,修改types/__init__.py
中的get_instance
方法,其能再接收一个option
参数。
def get_instance(meta_type: str, option: dict):
# 获取较验类的实例对象
m, c = meta_type.rsplit(‘.‘, maxsplit=1)
mod = importlib.import_module(m) # 动态加载模块
cls = getattr(mod, c) # 反射,获取c对象
obj = cls(option) # 向较验类传递option参数
if isinstance(obj, BaseType):
return obj
raise TypeError(‘{} is not BaseType‘.format(obj))
既然obj = cls(option)
,那动态加载的类对象也得接收一个参数,而类对象继承BaseType
基类,所以在基类上增加可接收一个option
参数,并实现__getattr__
方法,方便调用。
class BaseType:
def __init__(self, option):
self.option = option
def __getattr__(self, item):
return self.option.get(item)
def stringify(self, value):
# 从用户端拿到数据,转换成字符串,基类未实现
raise NotImplementedError()
def destringify(self, value):
# 从数据库拿数据,返回。还原数据的过程,基类不实现
raise NotImplementedError()
Int类也把最小值最大值的判断加入,如下:
class Int(BaseType):
"""
整形类型及满园较验
"""
def stringify(self, value):
try:
val = int(value)
except Exception as e:
raise TypeError(‘{} is not like digit.‘.format(value))
if self.option is not None and self.option:
_max = self.max
_min = self.min
if _max and val > _max:
raise ValueError(‘Too big.‘)
if _min and val < _min:
raise ValueError(‘Too small.‘)
return str(val) # 最后要存放在value表的value字段里,所以要str
def destringify(self, value):
return value
如果对整形数据没有范围的限制,那meta
字段中的option
是一个空对象,最好不省略。
如果要对IP做前缀匹配,那meta
字段类似如下:
{
"type":"dbapi.types.IP",
"option": {
"prefix": "192.168"
},
"value": "192.168.1.32"
}
同样需要修改IP较验的类
class IP(BaseType):
# 检验IP地址
def stringify(self, value):
try:
val = ipaddress.ip_address(value)
except Exception as e:
raise ValueError(‘{} does not look like ip address.‘.format(value))
if self.option is not None and self.option:
print(self.option)
if not str(val).startswith(str(self.prefix)):
raise ValueError(‘Must startswith {}‘.format(self.prefix))
return str(val)
return str(val) # 无前缀约束时返回
def destringify(self, value):
return value
如果有其它的字段需要加上类似的约束,那按照上边的逻辑处理即可。
现在对get_instance
函数可以进行进一步的抽象,把动态加载模块功能与实例化功能分开。
def get_instance(meta_type: str, option: dict):
# 获取较验类的实例对象
m, c = meta_type.rsplit(‘.‘, maxsplit=1)
mod = importlib.import_module(m) # 动态加载模块
cls = getattr(mod, c) # 反射,获取c对象
obj = cls(option) # 向较验类传递option参数
if isinstance(obj, BaseType):
return obj
raise TypeError(‘{} is not BaseType‘.format(obj))
以上函数可以分解为以下的两个函数:
def get_cls(meta_type: str):
m, c = meta_type.rsplit(‘.‘, maxsplit=1)
mod = importlib.import_module(m) # 动态加载模块
cls = getattr(mod, c) # 反射,获取c对象
if not issubclass(cls, BaseType):
raise TypeError(‘{} is not BaseType‘.format(obj))
return cls
def get_instance(meta_type: str, option: dict):
return get_cls(meta_type)(option)
先明确一个概念,同名的模块多次进行加载操作,只加载一次。
比如有两个逻辑表
中的一个字段同时都定义了对值进行int
类型的约束,那他们各自都会执行到get_cls
函数进行types
模块的加载,但实质是只要加载过一次,那后边的都会延用前边已加载的模块,python会先去sys.modules
中查找相应的模块是否已经加载,如果未加载才进行加载模块的操作。而对于我们的业务系统,这个过程可以更加优化,自己来记录哪个模块已加载,如下代码
classes_cache = {}
def get_cls(meta_type: str):
cls = classes_cache.get(meta_type)
if cls:
return cls
m, c = meta_type.rsplit(‘.‘, maxsplit=1)
mod = importlib.import_module(m) # 动态加载模块
cls = getattr(mod, c) # 反射,获取c对象
classes_cache[meta_type] = cls # 写缓存
if not issubclass(cls, BaseType):
raise TypeError(‘{} is not BaseType‘.format(obj))
return cls
获取实例对象函数如下:
def get_instance(meta_type: str, option: dict):
return get_cls(meta_type)(option)
同样每次用户提交meta
元数据中有约束定义时都会实例化一个对象,即使两个约束规则一样。所以这里也需要增加缓存来加速,那key应该如何选择?还是单纯使用meta
中的type
合适吗?要弄清楚这个问题先看一个meta
,如下:
{
"type":"dbapi.types.Int",
"option": {
"min": 10,
"max": 100
},
"value": 80
}
如果option
中的约束更改了,如果以type
为key,那实例在进行约束判断时就不对了,而且"option":{"min": 10, "max": 100}
与"option":{"max": 100, "min": 10}
应该是视为相同的。所以需要对这个key做好设计。
instances_cache = {}
def get_instance(meta_type: str, option: dict):
key = ",".join("{}={}".format(k, v) for k, v in sorted(option.items()))
key = "{}|{}".format(meta_type, key)
obj = instances_cache.get(key)
if obj:
return obj
obj = get_cls(meta_type)(option)
instances_cache[key] = obj
return obj
key = ",".join("{}={}".format(k, v) for k, v in sorted(option.items()))
巧妙的将option
进行排序后,把kv键值对使用逗号连接起来,最后再与type
结合,这就形成了一个独一的key。
缓存的引入并不会能有立竿见影的效果,特别是对类对象的缓存,即使不做缓存,实质也不会加载多次模块,只是要培养这种建立缓存的思想,在开发中一见到有某个对象可能会被多次创建时就应该条件反射想到是否需要缓存。对实例对象进行缓存可以使用之前创建过的实例,如果没有这个缓存,每一次调用都是新创建实例对象,实例对象被使用后就不会再被使用,此时管理垃圾回收的引用计数器就会在该对象上减去1,当引用计数器为0时,该对象的生命就结束了,GC在进行垃圾回收时就会把相应的对象进行回收,如果有大量的实例对象被创建,又需要被回收,那GC运行时消耗资源越多,时间也占用越长,所以引入缓存是比较合适的。
meta中的dbapi.types.Int
这个type值太长了,不好记忆,能否写成Int
这样的短名称,这里需要与用户约定好,dbapi/types/__init__.py
中的类名可以直接拿来当做type
的值。既然这样,那短名称也要对应到相应的类对象上,那该如何处理?
在python中,作用域是无法突破当前模块的,当一个python包
被加载时会执行__init__.py
中的语句,有什么方法能知道当前模块中所有的变量及对象呢?globals
方法就可以,先看一个小测试:
class A:
X = 10
def fn():
a = 100
pass
m = 100
print(globals())
运行后输出
{‘__name__‘: ‘__main__‘, ‘__doc__‘: None, ‘__package__‘: None, ‘__loader__‘: <_frozen_importlib_external.SourceFileLoader object at 0x10fe6f780>, ‘__spec__‘: None, ‘__annotations__‘: {}, ‘__builtins__‘: <module ‘builtins‘ (built-in)>, ‘__file__‘: ‘/Users/Admin/Documents/python/cmdb_django/test.py‘, ‘__cached__‘: None, ‘A‘: <class ‘__main__.A‘>, ‘fn‘: <function fn at 0x10fde6e18>, ‘m‘: 100}
‘A‘: <class ‘__main__.A‘>, ‘fn‘: <function fn at 0x10fde6e18>, ‘m‘: 100
都被打印出来,那就可以使用global
方法把types
模块加载时运行__init__.py
的特性把该模块中的验证约束的类对象全部收集后放入到classes_cahce
这个缓存字典中,这样只要该模块被加载,那该模块中的类就在缓存中就有了。
在type/__init__.py
最后增加一个方法
def inject_classes_cache():
"""
类缓存函数
:return: None
"""
for k, v in globals().items():
if type(v) == type and issubclass(v, BaseType) and not k.startswith(‘BaseType‘):
classes_cache[k] = v # 短名称缓存
classes_cache[‘{}.{}‘.format(__name__, k)] = v # 长名称缓存
# 此模块被导入时注入较验数据类型的class
inject_classes_cache()
inject_classes_cache()
需要放在该文件的最后,只要该模块被加载就执行该函数,也就把较验证约束相关的类对象注入到了classe_caches
字典中。那get_cls
函数中的动态加载模块的代码就不必了,该函数修改为
def get_cls(meta_type: str):
"""
:param meta_type: 数据库中field表中meta字段中json字符串中的type值
:return: class
"""
cls = classes_cache.get(meta_type)
if cls:
return cls
# 如果缓存里未注入的类都视为非法的
raise TypeError(‘Wrong Type {}. Not subclass of BaseType.‘.format(meta_type))
get_instance
函数也作了小的调整,如下:
def get_instance(meta_type: str, option: dict = None):
"""
:param meta_type:
:param option: meta中的option
:return: 通过对type字串的解析处理后返回一个类型转换的实例
"""
if option is not None and option: # option字典不为None且不为空字典
key = ",".join("{}={}".format(k, v) for k, v in sorted(option.items()))
key = "{}|{}".format(meta_type, key)
obj = instances_cache.get(key)
if obj:
return obj
obj = get_cls(meta_type)(option)
instances_cache[key] = obj
return obj
else:
key = meta_type
obj = get_cls(meta_type)(option)
instances_cache[key] = obj
return obj
到目前,value
表里的value
字段里都是存放的一个单值。对这样的值一般可以设置是否可为空(nullable),是否是唯一(unique)的约束。
在meta
字段中增加nullable
选项,如果有此选项就需要应用到值的验证上。
如果设置为false
,表示值不可以为空,如果为空就抛出异常;如果为true
,表示可为空,那就什么也不做。
unique
选项,如果设置此选项就应用到值的验证上。
如果设为为false
,就不需要检查值的唯一性,直接插入值即可;如果设置为true
,就需要检查值的唯一性。
如何检查值的唯一性?虚拟表
由schema + field
组成,entity
表使用同一个schema_id
就表示这是同一张表的数据。例如有以下数据:
mysql> select * from `schema`;
+----+-------+------------+
| id | name | desc |
+----+-------+------------+
| 1 | host | host table |
| 3 | phone | 手机 |
+----+-------+------------+
2 rows in set (0.00 sec)
mysql> select * from field;
+----+----------+------+-----------+
| id | name | meta | schema_id |
+----+----------+------+-----------+
| 1 | hostname | NULL | 1 |
| 2 | ip | NULL | 1 |
| 4 | imei | | 3 |
| 5 | price | | 3 |
+----+----------+------+-----------+
4 rows in set (0.01 sec)
mysql> select * from entity;
+----+--------------+-----------+
| id | key | schema_id |
+----+--------------+-----------+
| 1 | uuid00000001 | 1 |
| 2 | uuid00000002 | 1 |
+----+--------------+-----------+
2 rows in set (0.01 sec)
mysql> select * from value;
+----+-------------+----------+-----------+
| id | value | field_id | entity_id |
+----+-------------+----------+-----------+
| 1 | webserver01 | 1 | 1 |
| 2 | 192.168.0.1 | 2 | 1 |
| 3 | webserver02 | 1 | 2 |
| 4 | 192.168.0.2 | 2 | 2 |
+----+-------------+----------+-----------+
4 rows in set (0.00 sec)
上边的信息表示在schema_id
为1的这个逻辑表
中有两条记录,如果field
表中id
为2的ip
字段要求唯一性约束
,如果要再插入一个value=‘192.168.0.2‘
的数据,这时就需要对要插入的‘192.168.0.2‘做唯一性检查。
schema_id
和field_id
,即要确定哪个逻辑表的哪个字段需要做唯一性检查entity
表中schema_id
为1的数据,表示虚拟表host的记录,查询sql为“select id from entity where schema_id = 1”value
表中的field_id=2
的所有entity_id
,查询sql为“select entity_id from value
where field_id = 2;”entity_id
在第2步查询的id
中,说明这些数据是host逻辑表的数据,查询sql为“SELECT * from value WHERE field_id = 2 and entity_id in ( SELECT id from entity where schema_id = 1);”最后统计一下查询出数据的条数,sql最后为“SELECT count(value) from value WHERE field_id = 2 and entity_id in ( SELECT id from entity where schema_id = 1) and value=‘192.168.0.2‘;”,如果结果为0,说明数据库中还没有这个ip地址,如果结果为1,说明数据库中已有此ip地址,如果大于1,说明数据库已经有重复的数据了。
另一种思路分析:
field
表中的每个字段是唯一
属于一个逻辑表的,那只要确定了逻辑表的字段的id,那在value
表中的以这个id
来过虑数据,得到的就是一个逻辑表指定的字段的数据,同样对这个value
字段进行统计同样能知道是否有重复数据。sql如为“select count(value) from value where field_id = 2 and value = ‘192.168.0.2‘;”。
要实现nullable
和unique
,需要在meta
字段中增加相应的选项,如下:
{
"type":"dbapi.types.IP",
"nullable": false,
"unique": true,
"option": {
"prefix": "192.168"
},
"value": "192.168.0.2"
}
如果一个主机绑定了多个IP地址,应该如何在逻辑表中记录。
value
表中的value
字段中存放类似192.168.0.1,192.168.0.10
来表示多值,这种实现简单,但修改,做唯一性约束困难。value
表中使用相同的entity_id
与field_id
来存储,即表示在一条插入的记录中在相应的字段中有两行记录,这样修改,做唯一性约束都比较容易实现。因为之前entity_id
和field_id
做了unique
索引,与现在的设计相违背,所以需要删除这个unique
索引。multi=false 默认为单值
用户提交一个值,如果数据库已存在就更新,不存在就新增加。
如:一个主机只能有一个设备编号,用户提交上来的数据时,不存在时就新增加,已存在就更新。
multi=true 多值
多值时逻辑较为复杂,比如主机的ip字段可以为多值。
假设该主机ip字段目前为空,无记录,现在提交了多个Ip上来
如果该主机已记录有ip,又提交上来了ip,那需要分情况考虑
2.1. 提交的ip都是新的ip
2.2. 提交的ip部分是新的ip
2.3. 提交的ip全部都是已存在的ip
这里的设计逻辑如下:
如果之前为空,现在提交多个ip上来,直接 增加。
如果之前数据库中有ip记录,先查一下Ip的条目,为C1;提交上来的数目为C2。那:
c1 == c2时,循环迭代,用c2的ip替换c1的
c1 < c2 时,循环迭代,替换c1个数据,再新增c2-c1个
c1 > c2 时,循环迭代,替换c2个数据,剩下c1-c2个删除
允许多值时meta
字段内容类似如下:
{
"type":"dbapi.types.IP",
"nullable": false,
"unique": true,
"option": {
"prefix": "192.168"
},
"multi": true,
"value": "192.168.0.1,192.168.0.2"
}
在meta
字段中提供reference
来表示与另一个逻辑表的关系。类似如下:
{
"type":"dbapi.types.IP",
"nullable": false,
"unique": true,
"option": {
"prefix": "192.168"
},
"multi": true,
"reference": {
"schema":1,
"field":2
},
"value": "192.168.0.1,192.168.0.2"
}
上边的意思就是该字段引用schema
中id为1的逻辑表的对应字段为field
中id为2的字段。
比如有一个名为host的逻辑表和一个名ipaddress的逻辑表,host表中的数据需要在ipaddress中存在,也就是外键引用的关系。host表我们叫做source表,ipaddress表叫做target表。
如果两个逻辑表的字段增加了引用后,需要检查这两个字段的类型是否一致,即检查source表中的meta
中的type
定义与target表中定义的是否一致?如果一致,才能继续。
source表
target表
查询数据,直接查。
增加数据,新数据直接插入。
删除数据,可能已被引用,需要有删除策略。
3.1. 级联删除cascade,先查询host表中使用这个字段值的记录并删除它,然后再删除ipaddress表中的记录。
3.2. 置空set_null,例如:host.ip和switch.ip都引用了ipaddress.ip,ipaddress.ip要删除192.168.0.2,那么就要去value表先置空host表、switch表的引用。
3.3. 禁用disable,被引用了就不允许删除,host.ip和switch.ip都引用了ipaddress.ip,ipaddress.ip要删除192.168.0.2,如此此ip被引用了就不允许删除。
修改数据
4.1. 级联更新cascade,与删除类似,不过是把source表更新成新值,target表要先检验这个新值,source表也要通过自己字段的检验,否则抛异常。
4.2. 禁用disable,如果被引用,就更新失败。这里不用置空,因为target表的字段值与sorce表的字段应该保持一致。
source表的meta字段设计如下:
{
"type":"dbapi.types.IP",
"nullable": false,
"unique": true,
"option": {
"prefix": "192.168"
},
"multi": true,
"reference": {
"schema":1,
"field":2,
"on_delete": "cascade|set_null|disable", # 三值取一
"on_update": "cascade|disable" # 二值取一
},
"value": "192.168.0.1,192.168.0.2"
}
多表关联的外键约束设计,非常复杂,代码实现成本非常高,非常难控制。
如果使用了这种外键约束,target表的主键变动了,首先不知道谁引用了,需要代码去遍历field表的所有记录,从meta字段中解析谁引用了这个字段,非常没有效率。为了提高效率可以在field表增加一个字段reference,如果是source表中的字段,它引用了target表的字段,则这个reference字段一定有值,其值为target表的相应字段的id。
没影响,直接增加。
新增加一个字段到逻辑表里,这个字段是新的,还没有任何数据使用这个字段。那就要考虑这个字段能否为空?
nullable可否为空:
field
表中就只有两个字段可修改,一个是name
,另一个是meta
。
修改name,没问题,只是一个描述性字符串,value
表引用的是id。
修改meta
改type,要遍历相应逻辑表该字段的所有数据,拿出来转换成相应的类型,转换成功后再写回去。操作成本太高,而且很少有这种需求,因此,不提供此功能。
改nullable
从trure到false,即表示从可为空变为不可为空,就需要遍历该逻辑表的该字段,使用该字段在field表中的id在value表中查找value字段是否为null,或者缺失(因为在新增字段时,可以为空,就什么都不做)。如果为null或缺失,较验失败。查询sql为:select count(id) from value where field_id=2 and value is null or value = ‘‘; 如果count大于0,较验失败。
改unique
从true到false,什么都不做
从fase到true,就需要判断当前该逻辑表的该字段里是否有重复的数据,如何判断?
select count(id) from value where field_id = 2;
select distinct count(value) from value where field_id = 2;
如果两次的count结果一样,就无重复的数据,如果不一致,那就有重复数据。注意第二条语句执行不会太快。
改multi
从true到false,需要计算每一个entity_id、field_id的count,sql为:select count(id) from value where field_id = 2 and entity_id = 1; 如果count值大于1说明是多值,检验失败
改reference
增加引用,就是检查该字段(外键)的所有值是否出现在被引用逻辑表的主键上,只要有一个不在,检验就失败。如果检验成功,在reference字段上填上外键的信息。这些主键,外键值都存储在value表中,所以使用自关联。参考下边的参考1
,如何使用自关联左联来做检验。
先truncate table
四张表,再插入以下的实验数据
-- 增加host逻辑表
INSERT INTO `schema` (name) VALUES (‘host‘);
-- 增加ippool逻辑表
INSERT INTO `schema` (name) VALUES (‘ippool‘);
-- SELECT * FROM `schema`
-- ippool逻辑表增加名称为ip的字段
INSERT INTO field (name, schema_id) VALUES (‘ip‘, 2);
-- ippool逻辑表增加三条记录
INSERT INTO entity (`key`, schema_id) VALUES (‘uuid01‘, 2);
INSERT INTO entity (`key`, schema_id) VALUES (‘uuid02‘, 2);
INSERT INTO entity (`key`, schema_id) VALUES (‘uuid03‘, 2);
INSERT INTO `value` (`value`, entity_id, field_id) VALUES (‘192.168.1.10‘, 1, 1);
INSERT INTO `value` (`value`, entity_id, field_id) VALUES (‘192.168.1.20‘, 2, 1);
INSERT INTO `value` (`value`, entity_id, field_id) VALUES (‘192.168.1.30‘, 3, 1);
-- 逻辑表host增加两个字段
INSERT INTO field (name, schema_id) VALUES (‘hostname‘, 1);
INSERT INTO field (name, schema_id) VALUES (‘ip‘, 1);
-- 逻辑表host增加两条记录
INSERT INTO entity (`key`, schema_id) VALUES (‘uuid04‘, 1);
INSERT INTO `value` (`value`, entity_id, field_id) VALUES (‘dns server‘, 4, 2);
INSERT INTO `value` (`value`, entity_id, field_id) VALUES (‘172.16.100.1‘, 4, 3);
INSERT INTO entity (`key`, schema_id) VALUES (‘uuid05‘, 1);
INSERT INTO `value` (`value`, entity_id, field_id) VALUES (‘dbserver01‘, 5, 2);
INSERT INTO `value` (`value`, entity_id, field_id) VALUES (‘192.168.1.20‘, 5, 3);
SELECT source.*, target.*
FROM `value` AS source LEFT JOIN `value` AS target
ON source.`value` = target.`value` AND target.field_id = 1
WHERE source.field_id = 3 AND target.`value` is NULL
# 查询结果为
+----+--------------+----------+-----------+------+-------+----------+-----------+
| id | value | field_id | entity_id | id | value | field_id | entity_id |
+----+--------------+----------+-----------+------+-------+----------+-----------+
| 5 | 172.16.100.1 | 3 | 4 | NULL | NULL | NULL | NULL |
+----+--------------+----------+-----------+------+-------+----------+-----------+
如何理解这条sql?
把source当作host逻辑表,target当作ippool逻辑表,在host逻辑表上左关联ippool逻辑表,条件为value字段值相等,并且ippool逻辑表的字段为ip,即field_id为1,这样查出来的结果为:
mysql> SELECT source.*, target.* FROM `value` AS source LEFT JOIN `value` AS target ON source.`value` = target.`value` AND target.field_id = 1;
+----+--------------+----------+-----------+------+--------------+----------+-----------+
| id | value | field_id | entity_id | id | value | field_id | entity_id |
+----+--------------+----------+-----------+------+--------------+----------+-----------+
| 1 | 192.168.1.10 | 1 | 1 | 1 | 192.168.1.10 | 1 | 1 |
| 2 | 192.168.1.20 | 1 | 2 | 2 | 192.168.1.20 | 1 | 2 |
| 7 | 192.168.1.20 | 3 | 5 | 2 | 192.168.1.20 | 1 | 2 |
| 3 | 192.168.1.30 | 1 | 3 | 3 | 192.168.1.30 | 1 | 3 |
| 4 | dns server | 2 | 4 | NULL | NULL | NULL | NULL |
| 5 | 172.16.100.1 | 3 | 4 | NULL | NULL | NULL | NULL |
| 6 | dbserver01 | 2 | 5 | NULL | NULL | NULL | NULL |
+----+--------------+----------+-----------+------+--------------+----------+-----------+
这样就能找出host逻辑表中所有的ip字段与ippool表中ip字段值相等的记录,这种记录是左右对齐的;host表的其他字段会出现在左边,右边为NULL,如果host表的ip字段的值与ippool表中的ip字段的值不相等时也会出现在左边,右边NULL,如上边的172.16.100.1
,这就能说明host逻辑表中的ip字段有值不在ippool逻辑表中,如果要在host逻辑表的ip字段中增加外键引用ippool逻辑表的ip字段,那检验就不能成功。再进一步完善上边的sql,如下:
mysql> SELECT source.*, target.* FROM `value` AS source LEFT JOIN `value` AS target ON source.`value` = target.`value` AND target.field_id = 1 WHERE source.field_id = 3 AND target.`value` is NULL;
+----+--------------+----------+-----------+------+-------+----------+-----------+
| id | value | field_id | entity_id | id | value | field_id | entity_id |
+----+--------------+----------+-----------+------+-------+----------+-----------+
| 5 | 172.16.100.1 | 3 | 4 | NULL | NULL | NULL | NULL |
+----+--------------+----------+-----------+------+-------+----------+-----------+
正好找出一条数据不在ippool逻辑表中,对结果做一下统计
mysql> SELECT count(source.id) FROM `value` AS source LEFT JOIN `value` AS target ON source.`value` = target.`value` AND target.field_id = 1 WHERE source.field_id = 3 AND target.`value` is NULL;
+------------------+
| count(source.id) |
+------------------+
| 1 |
+------------------+
只要这个count的值不等于0,那就检验失败。
在生产环境中删除数据需要谨慎,一般会采取逻辑删除数据,所以在schema,field,entity,value
四个表中增加deleted
字段,默认值为0,表示未删除,1表示已删除。
数据做逻辑删除后带来了一个问题:
schema表中设置了name为unique,但是如果删除一个逻辑表后,加入一个同名的逻辑表名,就会报错。
field表中使用了schema_id和name构成unique,也一样有这种问题。
问题的关键在于名称冲突,而名称又是展示给用户看的,所以加上deleted字段构成unique。这样看似解决了删除一个名称,再次输入同名的问题,但是解决不了这个名称再次删除后的unique冲突。
所以依靠物理表的unique,不好解决这个问题,还需要自己编码最终解决这个问题。
一种解决思路:deleted字段中存放删除时的timestamp字符串,把deleted字段组合一起构成unique约束就可以了。
另一种思路:schema表删除name字段的唯一键约束,新增加deleted和deleted_date字段。当删除一个逻辑表时,置deleted字段为1,并且deleted_date记录删除时间,只要查询schema表中deleted=0,且deleted_date为空
的条目数为0就代表想新增的name可以正常增加。
meta字段处理
{
"type": {
"name": "dbapi.types.IP",
"option": {
"prefix": "192.168"
}
},
"nullable": false,
"unique": true,
"default": "",
"multi": true,
"reference": {
"schema":"ippool",
"field":"ip",
"on_delete": "cascade|set_null|disable", # 三值取一
"on_update": "cascade|disable" # 二值取一
}
}
type的简化写法
{
"type": "dbapi.types.IP",
"unique": true
}
也就是type可以是一个字符串,也可以是一个对象。
标签:doc main 文件 理解 def phone gem 加ip 哪些
原文地址:https://blog.51cto.com/zhaochj/2529372