Home  >  Article  >  Database  >  数据库技术-数据库命名与设计规范

数据库技术-数据库命名与设计规范

WBOY
WBOYOriginal
2016-06-07 15:28:221509browse

数据库技术-数据库命名与设计规范 数据库开发历史上一直使用一个有点神秘的系统命名数据库表和字段。最初的数据库管理系统(DBMS ) ,这些命名方案的限制的结果已成为惯例和传统。然而,随着数据库应用程序变得越来越复杂,更多的表和更大的开发团队,并为

数据库技术-数据库命名与设计规范


数据库开发历史上一直使用一个有点神秘的系统命名数据库表和字段。最初的数据库管理系统(DBMS ) ,这些命名方案的限制的结果已成为惯例和传统。然而,随着数据库应用程序变得越来越复杂,更多的表和更大的开发团队,并为开发人员来来去去,它变得更加重要,实现数据库对象一个强大的,有纪律的命名方案。一个良好定义的命名方案,当你采用对象关系映射(ORM )技术或自动代码生成变得更加重要。

 

命名表

 

在大多数数据库中,有三种类型的表:

1、数据表(Data Tables - 当然,在一个数据库中所有表包含数据,但我使用这个词来指代实际存储,而我们被刺激生成数据库,开始与数据,如客户,订单或产品的表。例如,一个客户表包含与如姓名,地址和电话领域的有关客户的信息。客户是一个数据表 - 不像其他这些表类型。

2、链接表(Link Tables)- 链接表做的无非就是连接两个不同的数据表的两个关键领域,形成许多一对多的关系。例如,你可以有一个供应商表和产品表之间的许多一对多的关系,因为每个供应商可以支持多个产品,每个产品可以通过多个供应商进行销售。这将需要一个第三表向卖方链接到的产品。

3、选择表(Picklist Tables - 这是常见的有内含的选择在数据表中的字段列表的表。例如,你可能在你的供应商表中的状态字段。值对供应商的地位可以从另一个表中选择。我指的是这些类型的表作为“选择”表,因为它们允许用户从列表中选择。

在我的命名方案,我想每个前缀的表名与三个前缀之一,以指示表的类型。我用下面的前缀:

 

数据表 - 我用的前缀TBL 。所以,记住我的规则大小写混合的,没有下划线,你可以有以下数据表:

tblCustomer

tblOrder

tblOrderEntry

tblVendor

tblProduct

链接表 - 我用的是前缀的链接。因此,与产品供应商联系起来,你将有一个表linkVendorProduct 。

 

选择表 - 我用的前缀pltbl 。因此,对于供应商的地位,你将有一个名为pltblVendorStatus表。如果你也有对客户状态的表,你可以有pltblCustomerStatus。

优点:

我发现这个表命名系统具有几个优点。

 

1、显然,这是很容易从表名告诉是包含什么样的数据表。

2、我所见过的(如Microsoft Access或SQL Server ),每个数据库的应用程序,按字母顺序列出你的表。使用这个前缀方案会使你的表是由类型时,按字母顺序显示分组。

3、如果你开发任何种类的自动代码生成工具,很容易通过编程从什么样的数据表中包含的表名来确定。您只需检查前缀。

单数/复数名

 

请注意,在上面我的数据表,所有的表名是单数,即tblCustomer而不是tblCustomers 。无论您喜欢单数或复数名称,你应该总是使用一个或另一个一致。我更喜欢单数,因为它似乎更清洁的给我。

 

其他表类型如:

 

记录表(log tables)

错误表(error tables)

系统表(system tables)

 

每个这些可以有他们自己的前缀。

基本命名规则

表1. 基本数据库对象命名

数据库对象 前缀 举例
表(Table)
字段(Column)
视图(View)
存储过程(Stored procedure)
触发器(Trigger)
索引(Index)
主键(Primary key)
外键(Foreign key)
Check约束(Check Constraint)
Unique约束
用户定义数据类型(User-defined data type)
用户定义函数(User-defined function)


v
pr
tr
ix_
pk_
fk_
ck_
uq_
udt
fn
Student
Title
vActivity
prDelOrder
trOrder_D
ix_CustomerID
pk_Admin
fk_Order_OrderType
ck_TableColumn
uq_TableColumn
udtPhone
fnDueDate

关于命名的约定

变量(T-SQL编程中声明的变量)、过程(存储过程或触发器等)、实体(表、字段)应该根据他们所代表的实体意义和进程作用来命名:

表2.好的命名 和 不好的命名 范例

好的命名 不好的命名
@CurrentDate
@ActivityCount
@EquipmentType
prCalculateTotalPrice
@D
@ActNum
@ET
@prRunCalc

还有一个常见的错误就是只使用面向计算机的术语,而不是面向公司业务的术语,比如ProcessRecord就是一个含糊不清的命名,应该使用一个进程业务描述来替换它,比如CompleteOrder.

如果完全根据上一条的要求,那么根据业务描述的过程名可能会变得很冗长,比如下面:

prCountTotalAmountOfMonthlyPayments (计算每月付费的总金额)

prGetParentOrganizationalUnitName (获取上级单位名称)

此时则应该考虑使用缩写:

  • 如果可以在字典里找到一个词的缩写,就用这个做为缩写,比如:Mon(Monday)、Dec(December)
  • 可以删除单词元音(词首字母除外)和每个单词的重复字母来缩写一个单词。比如:Current = Crnt、Address = Adr、Error = Err、Average = Avg
  • 不要使用有歧异的缩写(一般是语音上的歧义)。比如b4(before)、xqt(execute),4tran(Fortran

    避免无谓的表格后缀

    这两点我想大家都知道:1、表是用来存储数据信息的。2、表是行的集合。那么如果表名已经能够很好地说明其包含的数据信息,就不需要再添加体现上面两点的后缀了。

    实际工作中,我看到有的同事对表这样命名:GuestInfo,用于存储客户信息。这个命名与上面所说的第1点重复,谁都知道表本来就是存储信息(information)的,再加个Info无异于画蛇添足,个人认为直接用Guest做表名就可以了。

    对于存储航班信息的表,他又命名为FlightList。这个命名又与之前说的第2点相重复,表是行的集合,那么自然是列表(List),加上List后缀显得很多余,命名为 Flight 不是很好么?可见,他给自己都没有订立一个明确的命名规则,不然这两个表一定是要么命名为:GuestList、FlightList 要么命名为 GuestInfo、FlightInfo,而不会是两者的混合。

    多对多关系中连接表的命名

    大家知道,如果要实现两个实体间的多对多关系,需要三张表,其中一张是解析表。考虑下面这样一个多对多关系,这是一个经典的学生选课问题:一个学生可以选很多门课,一门课可以有很多学生。此时为了实现上面的关系,就需要一张解析表(这张表只存储学生ID和课程ID,而学生的信息和课程信息分别存在各自的表中),这个表的起名,建议的写法是将两个表的表名合并(如果表名比较长可做简化),此处如 StudentCourse。这个表中字段分别命名为StudentId、CourseID(既是此表的复合主键,同时分别为连接Student表和Course表的外键,等下到主键和外键的命名处再说),这样就实现了学生和课程之间的多对多关系,当然,这个关系还可以加点额外的东西,比如给StudentCourse表中加AccessLevel字段,值域D{只读,完全,禁止},就可以实现访问级别。

    约定俗成的字段名前/后缀

    数据库开发的时间久了,慢慢就会摸索出一个规律来:就是很多的字段都有些共同的特性。比如说,有的字段是代表时间的(例如发帖时间,评论时间),有的是代表数量的(例如浏览数,评论数),有的是代表真假类型的(例如是否将博客随笔显示在首页)。对于这种同一类型的字段,应该使用统一的 前缀 或者 后缀去标识它。

    我们来举几个例子看得更明白一点。

    以大家都熟悉的论坛来说,需要记录会员最后一次登录的时间,这时候一般人都会把这个字段命名为LoginTime 或者 LoginDate。这时候,已经产生了一个歧义:对于另一名开发者来说,如果仅看表的字段名称,不去看表的内容,很容易将LoginTime理解成 登录的次数,因为,Time还有一个很常用的意思,就是次数。

    为了避免这种情况发生,应该明确的规定:所有表示时间的字段,统一以 Date 来作为结尾。

    我们经常需要统计发帖数、回帖数信息,这时候,开发人员通常会这样去命名字段:PostAmount、PostTime、PostCount,同样,由于Time的歧义,我们首先排除掉不使用PostTime作为字段名。接下来,Amount 和 Count 都可以表示计数的意思,用哪个合适呢?这里,我推荐使用Count。为什么呢?如果你做过Asp开发,相信一定知道 RecordCount 这个属性,命名的时候有一个原则:就是使用约定俗成的名称,而不要去自创名称。既然微软都用Count后缀来表示数目,我们为什么不呢?

    于是,所有表示数目的字段,都应该以Count作为结尾。将这一概念做以推广,很容易得出,浏览次数为 ViewCount,登录次数为LoginCount 等等。

    再举一个例子,我们很少在数据库里直接保存图片等二进制数据,通常是仅保存图片的URL路径;在文章管理系统中,如果是转载文章,也会用到记录文章出处的字段。个人建议所有代表链接的字段,均为Url结尾。于是,图片路径的字段命名为 ImageUrl,文章出处字段的命名为SourceUrl。

    最后一个例子,我们经常需要用到布尔值,比方说,这篇随笔要不要显示到首页,这篇随笔是不是保存到草稿箱等等。同样,按照微软的建议,布尔类型的值均以 Is、Has 或者 Can开头。

    如果让我来建表示是否将随笔放到首页的字段,它的名字一定是这样的:IsOnIndex

    类似的例子是很多的,我在这里仅举出典型的几个范例,大家可以自行拓展,如果我能起到一个抛砖引玉的作用就很满足了。

    字段命名时需注意的一个问题

    我发现有很多开发人员喜欢给字段加上表名作为它的前缀,举个例子,如果有个表叫User,那么他就会将这个表中的字段命名为:UserId、UserPassword、UserName、UserPhone 等等。个人认为,这是没有必要的,因为你已经确切的知道了这个表存储的是User的信息,那么其中的字段必然是针对于User的。而且,在Join连接操作中,你的SQL代码看上去也会更加的精简一些,诸如 [User].UserName = Aritcle.ArticleAuthor 这样的代码完全可以实现为 [User].Name = Article.Author。

    这里还存在一个特例,就是表的外键包含的字段。在这种情况下,我倾向于使用表名+ID 的方式,比如 CategoryId 、UserId 等。假设有表Article,那么它的主键我会命名为Id,关联用户表User的外键包含的字段,我会命名为UserId。之所以这样,是因为在语言(比如C#)中创建对象时,有时候会使用代码生成器(根据数据库的字段名生成对象的字段、属性名),此时生成的代码更规整一些。

    建表时需要注意的问题

    数据库不仅是用来保存数据,还应负责维护数据的完整性和一致性

    我看过很多的开发人员设计出来的数据库,给我的感觉就是:在他们眼里,数据库的作用就如同它的名称一样――仅仅是用来存放数据的,除了不得不建的主键以外,什么都没有...没有 Check约束,没有索引,没有外键约束,没有视图,甚至没有存储过程。

    在这里,我提出如下数据库设计的建议:

    1. 如果要写代码来确保表中的行都是唯一的,就为表添加一个主键。
    2. 如果要写代码来确保表中的一个单独的列是唯一的,就为表添加一个约束。
    3. 如果要写代码确定表中的列的取值只能属于某个范围,就添加一个Check约束。
    4. 如果要写代码来连接 父-子 表,就创建一个关系。
    5. 如果要写代码来维护“一旦父表中的一行发生变化,连带变更子表中的相关行”,就启用级联删除和更新。
    6. 如果要调用大量的Join来进行一个查询,就创建一个视图。
    7. 如果要逐条的写数据库操作的语句来完成一个业务规则,就使用存储过程。

      NOTE:这里我没有提到触发器,实践证明触发器会使数据库迅速变得过于复杂,更重要的是触发器难以调试,如果不小心建了个连环触发器,就更让人头疼了,所以我更倾向于根本就不使用触发器。

      以Not Null的思路建表

      我发现很多开发人员在建表的时候,如果要新建一个字段,他的思路是这样的:默认这个字段是可以为Null的,然后去判断是不是非要Not Null不可,如果不是这样,OK,这个字段可以为Null,接着继续进行下一个字段。结果往往是一张表除了主键以外所有的字段都可以为Null。

      之所以会有这样的思路,是因为Null好啊,程序不容易出错啊,你插入记录的时候如果不小心忘输了一个字段,程序依然可以Run,而不会出现 “XX字段不能为Null”的错误消息。

      但是,这样做的结果却是很严重的,也会使你的程序变得更加繁琐,你不得不进行一些无谓的空值处理,以避免程序出错。更糟的是,如果一些重要数据,比如说订单的某一项值为Null了,那么大家知道,任何值与Null相操作(比如加减乘除),结果都是Null,导致的结果就是订单的总金额也为Null。

      你可以运行下面的代码尝试一下:

      Select Null + 5 As Result

      你可能会说,就算我将字段设置成Not Null,但是它依然可以接受空字符串,这样一来在程序中还是要进行空值处理。请别忘了,数据库还赋予你一个强力武器,就是 Check 约束,当你需要确保一个字段既不可以为Null,又不可以为空的时候,可以这么写:

      ColumnName Varchar(50) Not Null Constraint ck_ColumnName Check(Len(ColumnName) > 0)

      所以,合理的思维方式应该是这样的:默认这个字段是 Not Null的,然后判断这个字段是不是非为Null不可,如果不是这样,OK,这个字段是Not Null的,进行下一个字段。

      一个建表的范例脚本

      个人空间的表的建立,其中的文章表是这样写的:

      Create Table Article
      (
      Id Int Identity(1,1) Not Null,
      Title Varchar(50) Not Null Constraint uq_ArticleTitle Unique,
      Keywords Varchar(50) Not Null,
      Abstract Varchar(500) Not Null,
      Author Varchar(50) Not Null Default '张子阳',
      Type TinyInt Not Null Default 0 Constraint ck_ArticleType Check(Type in (0,1,2)), -- 0,原创;1,编译;2,翻译
      IsOnIndex Bit Not Null Default 1, -- 是否显示在首页
      Content Text Not Null,
      SourceCode Varchar(100) Null, -- 程序源码的下载路径
      Source Varchar(50) Not Null Default 'TraceFact', -- 文章出处
      SrcUrl Varchar(150) Null, -- 文章出处的URL
      PostDate DateTime Not Null Default GetDate(),
      ViewCount Int Not Null Default 0,
      ClassId Int Not Null -- 外键包含的字段,文章类别

      Constraint pk_Article Primary Key(Id) -- 建立主键
      )

      可以看到,在这里我使用了 Check 约束,以确保文章类型只能为 0,1,2。这里,我想说的是Check 约束的命名规则:尽管Check约束是针对字段的,但在同一数据库中,却不能有同名的Check约束。所以,建议使用 ck_ + 表名 + 字段名 来命名它,比如这个范例脚本中的 ck_ArticleType。

      除此以外,我还使用了Unique约束,以确保文章标题的唯一性。由于这是我的博客文章表,不应该出现重复的题目,这样可以避免在使用 Insert 语句时插入重复值。类似于Check约束,这里的命名规则是:uq_ + 表名 + 字段名。

      主键的命名

      按照SQL Server 的默认规范(使用企业管理器创建主键时默认产生的主键名),主键的命名为 pk_TableName。主键是针对一个表的,而不是针对一个字段的,大家有时候在企业管理器中会见到一个表的两个字段前面都会有钥匙的图标(比如SQL Server 2000自带的NorthWind范例数据库的EmployeeTerritories表),就会误以为主键是针对字段的,即是说一个表上有两个主键,其实错了,只有一个主键,但包含了两个字段,这就是常说的复合主键。为了有个更生动的认识,看下建立复合主键的SQL语句,以上面说到的多对多连接表StudentCourse为例:

      Alter Table StudentCourse
      Add Constraint pk_StudentCourse Primary key(StudentId, CourseId)

      可见,对于主键pk_StudentCourse,包含了两个字段StudentId 和 CourseId。

      外键的命名

      外键的命名为 fk_外键所在的表名_外键引用的表名。因为外键所在的表为从表,所以上式可以写为 fk_从表名_主表名。

      外键包含的字段的命名,外键包含的字段和外键是完全不同的概念。外键包含字段的命名,建议为:外键所在的表名 + Id。

      考虑这样一个关系,表Hotel,字段Id, Name, CityId。表City,字段Id,Name。因为一个城市可能有好多家酒店,所以是一个一对多的关系,City是主表(1方),Hotel是从表(多方)。在Hotel表中,CityId是做为外键使用。

      在实现外键的时候我们可以这样写:

      Alter Table HotelInfo
      Add Constraint fk_HotelInfo_City Foreign Key (CityID) References City(ID)
      On Delete No Action On update No Action

      很明显,fk_HotelInfo_City是外键的名字,CityId是外键包含的字段的名字。

      NOTE:在创建数据库表的时候,一般需要写成三个SQL脚本文件。第一个文件仅包含所有的创建表的SQL语句,即Create Table 语句。第二个文件包含删除关系和表的语句,其中,所有删除关系的语句,即Drop Constraint 语句集中在这个文件的上半部分,所有删除表的语句,Drop Table语句,集中在这个文件的下半部分。第三个文件包含建立表之间关系的语句。这种做法会在你移植数据库的时候产生较大的便利,原因我就不解释了,您一试便知。

      而对于多对多关系中解析表的外键包含的字段,顺理往下推,我们可以这样写(再次回到学生选课的多对多例子中):

      建立解析表StudentCourse与Student表的外键关系:

      Alter Table StudentCourse
      Add Constraint fk_StudentCourse_Student Foreign Key (StudentId) References Student (Id)
      On Delete No Action On Update No Action

      建立解析表StudentCourse与Course 表的外键关系:

      Alter Table StudentCourse
      Add Constraint fk_StudentCourse_Course Foreign Key (CourseId) References Course(Id)
      On Delete No Action On Update No Action

      触发器的命名

      由三部分构成:

      1. 前缀(tr),描述了数据库对象的类型。
      2. 基本部分,描述触发器所加的表。
      3. 后缀(_I、_U、_D),显示了修改语句(Insert, Update及Delete)

        存储过程的命名

        大家知道,系统存储过程的前缀是 sp_,为了避免将用户存储过程与系统存储过程混淆,这里我推荐大家使用 pr 作为自己定义的存储过程的命名。

        同时,命名的规则是:采用自解释型的命名,比如:prGetItemById。

        这里,有个有意思的地方值得深思。我们按上面规则命名存储过程的时候,可以用两种方式:

        1. 动词放前面,名词放后面。
        2. 名词放前面,动词放后面。

          我个人推荐使用方式2,现在说说原因:

          以NorthWind 为例,假如对于 Employees 表你有4个存储过程,分别命名为:prEmployeeInsert、prEmployeeUpdate、prEmployeeDelById、prEmployeeGetById

          同时对于 Products 表你有类似的4个存储过程,分别命名为:prProductInsert、prProductUpdate、prProductDelById、prProductGetById

          这时,你用企业管理器查看时,会发现存储过程像下面这样整整齐齐的排列:

          prEmployeeDelById
          prEmployeeGetById
          prEmployeeInsert
          prEmployeeUpdate
          prProductDelById
          prProductGetById
          prProductInsert
          prProductUpdate

          很容易就会发现,当你的存储过程越多时,这种命名方法的优势就越明显。

          存储过程中参数的命名

          存储过程中的入口参数,我建议与其对应的字段名相同,这里,假设要写一个更新Northwind数据库Employees表的存储过程(做了简化),可以这么写:

          Create Procedure prEmployeeUpdateById
          @EmployeeId Int,
          @LastName NVarchar(20),
          @FirstName NVarchar(10)
          As
          Update Employees Set
          LastName = @LastName,
          FirstName = @FirstName
          Where
          EmployeeId = @EmployeeId

          If @@error 0 or @@RowCount = 0
          Raiserror 16001 ‘更新用户失败’

          参考

           

          2 数据库命名原则

          2.1 数据文件

          如果数据库采用文件系统,而不是裸设备,约定下列命名规则:

          1)数据文件以表空间名为开始,以.dbf为结尾,全部采用小写英文字母加数字命名。如该表空间有多个数据文件,则从第2个数据文件开始,在表空间名后加_。

          例:对system表空间的数据文件:system.dbf,system_2.dbf

          2)对oracle数据库的控制文件,用control.ctl来表示。如control01.ctl,control02.ctl。

          3)对oracle数据库的日志文件,在线日志文件用redo.dbf来表示。其中组名和文件序列名均用2位数字来表示。如第一组的两个文件表示位redo0101.dbf和redo0102.dbf。归档日志用arch_%t_%s.arc来表示。其中%t和%s均为oracle约定的变量。

          2.2 表空间

          2.2.1 数据库系统表空间

          数据库系统表空间包括system表空间,临时表空间,回滚段的表空间。约定下列命名规则:

          1)system表空间由数据库直接限定,不能进行修改。

          2)临时表空间用temp来表示。如果有多个临时表空间,从第2个临时表空间开始,在temp后面加来表示。

          3)回滚段表空间用undotbs来表示。如果有多个回滚段表空间,从第2个回滚段表空间开始,在undotbs后面加来表示。

          2.2.2 数据库的用户表空间

          数据库的用户表空间用ts_来表示。其中,表空间名分为:

          1)数据空间:对于用户的缺省表空间,用default来表示。对于其他的表空间,根据存放在表空间上的表的类别来表示。如放代码的表,用code来表示。放客户资料的表,用customer来表示。尽量用一个表空间来存放该类的表。如果某表特别大,可考虑单独使用一个表空间。

          2)索引空间:在相应的数据表空间的名字前加ind_。如对用户缺省表空间的索引空间,用ts_ind_default来表示。对代码表的索引表空间,用ts_ind_code来表示。

          2.3 表

          数据库表的命名采用如下规则:

          1)表名用T_开头,表名长度不能超过30个字符,表名中含有单词全部采用单数形式,单词要大写。

          2)多个单词间用下划线(_)进行连接。若库中有多个系统,表名采用系统名称+单词或多个单词,系统名是开发系统的缩写,如VNET。

          3)表中含有的单词建议用完整的单词。如果导致表名长度超过30个字符,则从最后一个单词开始,依次向前采用该单词的缩写。(如果没有约定的缩写,则采用该单词前4个字母来表示)。

          数据库表的字段命名采用如下规则:

          1)数据库字段名全部采用小写英文单词,单词之间用”_”隔开。字段长度不能超过30个字符。

          2)如果该字段是代码,则在单词后加_id。

          3)如果该字段表示的是时间,则使用_time为后缀。

          2.4 视图

          数据库视图的命名采用如下规则:

          1)视图名用V_开头,视图名长度不能超过30个字符。视图名用大写的英文单词来表示。

          2)视图由几个表产生就用下划线(_)连接几个表的名,如果表过多可以将表名适当简化,但一定要列出所有表名。

          2.5 序列

          数据库序列的命名采用如下规则:

          序列名用seq_开头,后面跟使用该序列的字段名。如果有几个字段用同一个序列,用下划线(_)连接几个字段的名称。如果不同表中相同的字段名需要使用不同的序列,则在字段名后加表的特征,用下划线(_)连接。序列名长度不能超过30个字符。序列名用小写的英文单词来表示。

          2.6 存储过程

          存储过程的命名采用如下规则:

          存储过程名用Pr_开头,存储过程名长度不能超过30个字符。存储过程名用小写的英文单词来表示。

          2.7 函数

          函数的命名采用如下规则:

          函数名用Fu_开头,函数名长度不能超过30个字符。函数名用小写的英文单词来表示。

          2.8 触发器

          触发器的命名采用如下规则:

          触发器名用Tr_开头,触发器名长度不能超过30个字符。触发器名用小写的英文单词来表示。

          2.9 主键

          主键的命名采用如下规则:

          主键名用pk_开头,后面跟该主键所在的表名。主键名长度不能超过30个字符。如果过长,可对表名进行缩写。缩写规则同表名的缩写规则。主键名用小写的英文单词来表示。

          2.10 外键

          外键的命名采用如下规则:

          外键名用fk_开头,后面跟该外键所在的表名和对应的主表名(不含t_)。子表名和父表名自己用下划线(_)分隔。外键名长度不能超过30个字符。如果过长,可对表名进行缩写。缩写规则同表名的缩写规则。外键名用小写的英文单词来表示。

          2.11 索引

          索引的命名采用如下规则:

          1)索引名用小写的英文字母和数字表示。索引名的长度不能超过30个字符。

          2)主键对应的索引和主键同名。

          3)每类索引都用_结束。

          4)唯一性索引用uni_开头,后面跟表名。一般性索引用ind_开头,后面跟表名。

          5)如果索引长度过长,可对表名进行缩写。缩写规则同表名的缩写规则。

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn