关系表命名约定


155

我正在开始一个新项目,并希望从一开始就获取我的表名和列名。例如,我一直在表名中使用复数形式,但是最近学习到的单数形式是正确的。

因此,如果我得到一个表“ user”,然后得到只有该用户拥有的产品,该表应该命名为“ user_product”还是“ product”?这是一对多的关系。

进一步,如果我(出于某种原因)对每个产品有几个产品描述,是“ user_product_description”还是“ product_description”还是“ description”?当然,要设置正确的外键。仅将描述命名是有问题的,因为我也可以具有用户描述或帐户描述等。

如果我想要一个只有两列的纯关系表(很多),该怎么办?“ user_stuff”还是“ rel_user_stuff”之类的?如果是第一个,那么这与“ user_product”有什么区别?

我们非常感谢您提供的任何帮助,如果有人建议您使用某种命名约定标准,请随时进行链接。

谢谢


20
(a)四年前提出并回答了这个问题。(b)问题和所选答案均获得高票。(c)我们有一个命名惯例标签(d)对于初学者来说,它可能是“基于观点的”,但是对于寻求者正在寻找的经验丰富的技术人员来说,这是一个标准问题。然而,给出了许多处方中的每一个的理由。(e)因此,根据证据,primarily opinion-based显然是错误的。
PerformanceDBA

对于经验丰富的技术人员或遇到过古老IDEF标准并认为它们是实际标准的人员,这是标准的问题。
gbr

1
此外,还有其他一些Qs re Naming Conventions,它们都具有价值。请参阅右侧列中的链接
PerformanceDBA

Answers:


385

表•名称

最近学到的单数是正确的

是。当心异教徒。表名中的复数是不读任何标准材料并且不懂数据库理论的人的肯定标志。

关于标准的一些奇妙的事情是:

  • 它们都彼此集成在一起
  • 他们一起工作
  • 它们是由比我们更伟大的思想写成的,因此我们不必辩论它们。

标准表名称是指表中的每一,所有中都使用该表,而不是表的总内容(我们知道该Customer表包含所有客户)。

关系,动词短语

在已建模的正版关系数据库中(与1970年代之前的记录归档系统相反(Record IDs为了方便起见,其特征在SQL数据库容器中实现):

  • 这些表是数据库的主题,因此它们又是名词,也是单数形式
  • 表之间的关系是名词之间发生的动作,因此它们是动词(即,它们没有被任意编号或命名)
  • 谓词
  • 所有可以直接从数据模型读取的内容(请参阅最后的示例)
  • (独立表(层次结构中最顶层的父表)的谓词是独立的)
  • 因此,动词短语是经过精心选择的,因此它是最有意义的,并且避免了通用术语(根据经验,这会变得更容易)。动词短语在建模过程中非常重要,因为它有助于解析模型。澄清关系,识别错误并更正表名。

图_A

当然,该关系在SQL中作为CONSTRAINT FOREIGN KEY子表(更多,稍后)实现。这是动词短语(在模型中),它表示的谓词(从模型中读取)以及FK 约束名称

    Initiates
    Each Customer Initiates 0-to-n SalesOrders
    Customer_Initiates_SalesOrder_fk

表•语言

但是,在描述表格时,尤其是使用诸如谓语之类的技术语言或其他文档进行描述时,请自然使用英语中的单数和复数形式。请记住,表是为单行(关系)命名的,语言是指每个派生的行(派生的关系):

    Each Customer initiates zero-to-many SalesOrders

    Customers have zero-to-many SalesOrders 

因此,如果我得到一个“用户”表,然后得到只有该用户拥有的产品,该表应该命名为“用户产品”还是“产品”?这是一对多的关系。

(这不是一个命名约定问题;这是一个数据库设计问题。)是否user::product为1 :: n 无关紧要。重要的是是否product是一个单独的实体以及它是否是一个独立表。它可以独立存在。因此product,不是user_product

并且如果product仅存在于的上下文中user,即。它是一个相关表,因此user_product

图_B

进而,如果我(出于某种原因)对每种产品有几个产品描述,是“用户产品描述”还是“产品描述”还是“描述”?当然,要设置正确的外键。仅将描述命名是有问题的,因为我也可以具有用户描述或帐户描述等。

那就对了。无论是user_product_description异或product_description将是正确的,基于以上。这并不是要与其他xxxx_descriptions名称区别开来,而是要使名称具有其所属位置的名称,前缀是父表。

如果我想要一个只有两列的纯关系表(很多),该怎么办?“用户资料”还是“ rel-user-stuff”之类的?如果是第一个,那么这与“用户产品”有什么区别呢?

  1. 希望关系数据库中的所有表都是纯的关系标准化表。无需在名称中标识该名称(否则所有表均为rel_something)。

  2. 如果它包含两个父级的PK(将不存在为逻辑级别的实体的逻辑 n :: n关系解析为物理表),则为关联表。是的,名称通常是两个父表名称的组合。

    • 请注意,在这种情况下,动词短语适用于父母对父母,并且在父母对父母之间被读为,而忽略了子表,因为它的唯一目的是将两个父母联系起来。

      图_C

    • 如果它不是关联表(即除了两个PK外,它还包含数据),则应适当命名,动词短语适用于它,而不是关系末尾的父代。

      图_D

  3. 如果最终有两个user_product表,那么这是一个非常响亮的信号,表明您尚未规范化数据。因此,请返回几步并执行此操作,然后准确一致地命名表。这些名称将自行解析。

命名约定

我们非常感谢您提供的任何帮助,如果有人建议您使用某种命名约定标准,请随时进行链接。

您正在做的事情非常重要,它将影响每个级别的易用性和理解力。因此,一开始就获得尽可能多的理解是一件好事。在您开始使用SQL进行编码之前,其中大部分的相关性还不清楚。

  1. 案例是第一个要解决的问题。所有上限均不可接受。混合大小写是正常的,特别是如果用户可以直接访问表。参考我的数据模型。请注意,当搜寻器使用的是痴呆的NonSQL(只有小写字母)时,我给出了这种情况,在这种情况下,我会包括下划线(根据您的示例)。

  2. 保持数据重点,而不是应用程序或使用重点。毕竟是2011年,我们从1984年开始拥有开放式体系结构,因此数据库应该独立于使用它们的应用程序。

    这样,随着它们的增长,并且不止一个应用程序使用它们,命名将保持有意义,并且无需更正。(完全嵌入单个应用程序中的数据库不是数据库。)仅将数据元素命名为data。

  3. 要非常体贴,并且要非常准确地命名表和列。UpdatedDate如果它是DATETIME数据类型,请不要使用UpdatedDtm_description如果它包含剂量,不要使用。

  4. 在整个数据库中保持一致很重要。请勿NumProduct在一个地方使用来表示产品数量,ItemNo或者ItemNum在另一个地方使用来表示商品数量。一致地NumSomething用于编号和/ SomethingNoSomethingId标识符。

  5. 请勿在列名前添加表名或短代码,例如user_first_name。SQL已经提供了表名作为限定符:

        table_name.column_name  -- notice the dot
    
  6. 例外情况:

    • 第一个例外是PK,它们需要特殊的处理,因为您一直都在联接中对它们进行编码,并且希望键从数据列中脱颖而出。永远使用user_id,永不id

      • 注意,这不是用作前缀的表名,但对于密钥的部件适当的描述性名称:user_id是列是标识用户,而不是id所述的user表中。
        • (当然,在记录归档系统中,代理可以访问文件并且没有关系密钥,它们是同一东西)。
      • 无论将PK作为FK进行(迁移)在哪里,请始终对键列使用完全相同的名称。
      • 因此,该user_product表将user_id在其PK中包含一个(user_id, product_no)
      • 当您开始编码时,其相关性将变得清楚。首先,id在多个表上,很容易混淆SQL编码。其次,其他人都不知道最初的编码人员会做什么。如果对键列按上述方法进行处理,则很容易防止这两种情况。
    • 第二个例外是在子级中包含多个引用同一父级表的FK。根据关系模型,使用角色名称来区分含义或用法,例如。AssemblyCodeComponentCode两个PartCodes。而在这种情况下,千万不能使用未分化PartCode的其中之一。精确点。

      图_E

  7. 前缀
    如果您有多于100个表,请在表名前加上主题区域:

    REF_用于
    OE_“订单输入”集群的参考表等

    仅在物理级别,而不是逻辑级别(它会使模型混乱)。

  8. 后缀
    切勿在表上使用后缀,而在其他所有内容上均使用后缀。这意味着在逻辑上正常使用数据库时,没有下划线。但在管理方面,下划线用作分隔符:

    _V视图(TableName当然,主视图在前面)
    _fk外键(约束名称,而不是列名称)
    _cac缓存
    _seg
    _tr事务(存储的proc或函数)
    _fn功能(非事务性),等等。

    格式是表或FK名称,下划线和操作名称,下划线,最后是后缀。

    这真的很重要,因为当服务器给您一条错误消息时:

    ____blah blah blah error on object_name

    您确切知道违反了什么对象,以及它试图做什么:

    ____blah blah blah error on Customer_Add_tr

  9. 外键(约束,而不是列)。FK的最佳命名是使用动词短语(减去“每个”和基数)。

    Customer_Initiates_SalesOrder_fk
    Part_Comprises_Component_fk
    Part_IsConsumedIn_Assembly_fk

    使用该Parent_Child_fk顺序,不是Child_Parent_fk因为(a)当您寻找它们时它以正确的排序顺序显示,并且(b)我们始终知道所涉及的孩子,我们猜测的是哪个父母。错误消息是令人愉快的:

    ____ Foreign key violation on Vendor_Offers_PartVendor_fk

    对于那些烦恼要对数据建模的人来说,这很有效,因为在这里已经找到了动词短语。对于其余部分,使用记录归档系统等Parent_Child_fk

  10. 索引是特殊的,因此它们具有自己的命名约定,按顺序从1到3的每个字符位置组成:

    U唯一,或_用于非唯一
    C群集,或_用于非群集
    _分隔符

    对于其余的:

    • 如果键是一栏或几栏:
      ____ColumnNames

    • 如果键超过几列:
      ____ PK主键(根据型号)
      ____ AK[*n*]备用键(IDEF1X术语)

    请注意,表名在索引名中不是必需的,因为它总是显示为table_name.index_name.

    因此,当错误消息中出现Customer.UC_CustomerId或时Product.U__AK,它会告诉您一些有意义的信息。当您查看表上的索引时,可以轻松区分它们。

  11. 寻找一个合格且专业的人并关注他们。查看他们的设计,并仔细研究他们使用的命名约定。向他们询问有关您不理解的任何问题的具体问题。反之,任何对命名约定或标准不怎么重视的人都会像地狱般奔跑。以下是一些入门指南:

    • 它们包含以上所有内容的真实示例。提出问题,在此线程中重新命名问题。
    • 当然,除了命名约定外,这些模型还实现了其他几个标准。您可以暂时忽略这些问题,也可以随时提出特定的新问题
    • 它们每个都是几页,Stack Overflow的在线图像支持是针对鸟类的,它们在不同的浏览器中加载不一致。因此您必须单击链接。
    • 请注意,PDF文件具有完整的导航,因此请单击蓝色玻璃按钮或识别出扩展的对象:
    • 对关系建模标准不熟悉的读者可能会发现IDEF1X表示法很有帮助。

具有标准地址的订单输入和库存

适用于PHP / MyNonSQL的简单部门间公告系统

具有完整时间功能的传感器监控

问题答案

在评论空间中无法合理回答。

拉里·卢斯蒂格(Larry Lustig):
...甚至最琐碎的例子也表明...
如果客户的产品数量为零,而产品的组件数量为一对,而一个组件的供应商数量为一对,而供应商的销售额为零对多组件和SalesRep有一对多客户持有客户,产品,组件和供应商的表的“自然”名称是什么?

您的评论有两个主要问题:

  1. 您声明自己的示例“最琐碎”,但是,仅此而已。由于存在这种矛盾,我不确定您是否认真,如果在技术上有能力。

  2. 这种“琐碎的”推测有几个严重的规范化(DB设计)错误。

    • 在纠正这些错误之前,它们是不自然且异常的,并且没有任何意义。您也可以将它们命名为异常_1,异常_2等。

    • 您有不提供任何东西的“供应商”。循环引用(非法的和不必要的);客户购买没有任何商业手段的产品(例如发票或SalesOrder)作为购买的依据(或者客户“拥有”产品?);未解决的多对多关系;等等

    • 将其标准化后,并识别所需的表,它们的名称将变得显而易见。自然。

无论如何,我都会尝试为您的查询提供服务。这意味着我将不得不对它有所了解,而不知道您的意思,所以请忍受我。总体错误太多,无法列出,并且给出了备用规范,我不确定是否已全部纠正。

  • 我将假设,如果产品由零部件组成,则该产品是一个装配体,并且这些零部件用于多个装配体中。

  • 此外,由于“供应商销售零对多组件”,因此他们出售产品或组件,因此它们仅出售组件。

投机与规范化模型

如果您不知道,则四角(独立)和圆角(相关)之间的差异非常大,请参阅IDEF1X符号表示链接。同样,实线(标识)与虚线(非标识)。

...持有客户,产品,组件和供应商的表的“自然”名称是什么?

  • 顾客
  • 产品
  • 组件(或者,AssemblyComponent,对于那些意识到一个事实可以识别另一个事实的人)
  • 供应商

现在我已经解决了表格,我不明白您的问题。也许您可以发布一个特定的问题。

VoteCoffee:
您如何处理Ronnis在他的示例中发布的方案,其中2个表(user_likes_product,user_bought_product)之间存在多个关系?我可能会误会,但是使用您详述的约定,这似乎导致表名称重复。

假设没有规范化错误,User likes Product是一个谓词,而不是一个表。不要混淆他们。请参阅我的答案(与主语,动词和谓语有关),以及我对拉里的回复。

  • 每个表含有一事实的(每一行是一个事实)。谓词(或命题)不是事实,它们可能正确也可能不正确。

    • 关系模型是基于一阶谓词演算(更通常被称为一阶逻辑)。谓词是简单,精确的英语单句,求值为真或假。

    • 此外,每个表代表许多谓词,或者是许多谓词的实现,而不是一个。

  • 查询是对谓词(或链接在一起的多个谓词)的测试,结果为true(事实存在)或false(事实不存在)。

  • 因此,应按照我的答案(命名约定)中的详细说明为表命名该行,事实和谓词(无论如何,它是数据库文档的一部分),但应作为谓词的单独列表。

  • 这并不表示它们不重要。它们非常重要,但我不会在这里写下。

  • 那快点 由于关系模型基于FOPC,因此整个数据库可以说是FOPC声明集,谓词集。但是(a)谓词的类型很多,并且(b)一张表不能代表一个谓词(它是许多谓词和不同类型的谓词的物理实现)。

  • 因此,将表命名为“该”谓词“代表”是一个荒谬的概念。

  • “理论家”仅了解少数谓词,他们不了解,因为RM是基于FOL建立的,所以整个数据库都是一组谓词,并且类型不同。

    • 当然,他们选择从一些荒谬的人,他们知道:EXISTING_PERSON; PERSON_IS_CALLED。如果不是那么难过,那将很有趣。

    • 还要注意,标准表或原子表名称(为行命名)对于所有谓词(包括表中附加的所有谓词)都很有效。相反,白痴的“表代表谓词”这个名字不能。对于“理论家”来说,这很好,他们对谓词了解得很少,但对智商却有所欠缺。

  • 是相关的数据模型的谓词,表示该模型中,它们是两个订单。

    1. 一元谓词
      第一组是图解而不是文本:表示法本身。这些包括各种存在的;约束导向;和描述符(属性)谓词。

      • 当然,这意味着只有那些能够“读取”标准数据模型的人才能读取那些谓词。这就是为什么“理论家”由于只能阅读纯文本的思维方式而严重瘫痪,而无法读取数据模型,为什么他们仍然坚持1984年前的只能阅读纯文本的思维方式。
    2. 二进制谓词
      第二组是在事实之间建立关系的谓词。这是关系线。动词短语(上面已详细介绍)标识已实施的谓词(命题)(可以通过查询进行测试)。没有比这更明确的了。

      • 因此,对于精通Standard数据模型的人,所有相关谓词都记录在模型中。他们不需要单独的谓词列表(但是不能从数据模型中“读取”所有内容的用户可以!)。
  • 这是一个数据模型,其中列出了谓词。我选择该示例是因为它显示了存在谓词,谓词以及关系谓词,未列出的唯一谓词是描述符。在这里,由于寻求者的学习水平,我将其视为用户。

因此,在两个父表之间有多个子表的事件不是问题,只需将它们命名为它们的内容的“存在事实”,然后对名称进行规范化即可。

我为关联表的关系名称提供的动词短语规则在这里起作用。这里是谓词与表的讨论,总结了所提到的所有要点。

要获得一个简短的简短说明,请正确使用谓词以及如何使用它们(与在此处答复评论完全不同的上下文),请访问此答案,然后向下滚动至谓词部分。


Charles Burns:
按照顺序,我的意思是按照某种规则(例如,“加1”)纯粹用于存储数字及其下一个的Oracle样式的对象。由于Oracle缺少自动ID表,因此我的典型用法是为表PK生成唯一的ID。插入foo(id,somedata)值(foo_s.nextval,“ data” ...)

好的,这就是我们所谓的Key或NextKey表。如此命名。如果您有SubjectAreas,请使用COM_NextKey指示它在整个数据库中是通用的。

顺便说一句,这是生成密钥的一种非常糟糕的方法。根本没有可扩展性,但是考虑到Oracle的性能,它可能“很好”。此外,它表明您的数据库中充满了替代项,而不是那些方面的关系。这意味着性能极差且缺乏完整性。


3
优秀的清理工作,谢谢。所有好的评论(加注星标,投票赞成)也都消失了。哦,给它时间,他们会回来的。
PerformanceDBA

3
帖子上有76条评论,应该将所有有价值的内容编辑成答案,因为几乎不可能从所有内容中提取任何内容。
塔琳

3
@PerformanceDBA。如“这是这样回答的我只是希望我能出演”没有评论应该不会被移动到了答案。应该包含的东西是要求澄清或指出错误的注释的答案
克里斯·

2
@ChrisF。(a)非常感谢您的解释,我不理解以前的主持人操作。(b)您是否可以重新打开该问题,尝试将其关闭显然是错误的(请参阅我对问题的评论)。否则,SO将继续失去良好的问题/答案,而新的问题将取代它们。帮助中指出:“我们不希望失去出色的答案!”。谢谢。
PerformanceDBA

12
您可以提供对这些“标准”中任何一个的参考吗?目前,这只是一个非常好的书面个人意见。
Shane Courtrille '16

18

单数与复数:选择一个并坚持使用。

列不应以前缀/后缀/前缀或通过引用事实来固定,因为它是列的事实。表也​​是如此。不要命名表EMPLOYEE_T或TBL_EMPLOYEES,因为第二个表被视图替换了,事情变得很混乱。

不要在名称中嵌入类型信息,例如varchar的“ vc_firstname”或“ flavor_enum”。也不要在列名称中嵌入约束,例如“ department_fk”或“ employee_pk”。

其实,关于*修正我能想到的,唯一的好处是,你可以用这样的保留字where_ttbl_orderuser_vw。当然,在这些示例中,使用复数将解决问题:)

不要将所有键都命名为“ ID”。指向同一事物的键在所有表中应具有相同的名称。在用户表和所有引用该用户的表中,用户ID列可以称为USER_ID。唯一的重命名是当不同的用户扮演不同的角色时,例如Message(sender_user_id,receiver_user_id)。在处理较大的查询时,这确实有帮助。

关于CaSe:

thisiswhatithinkofalllowercapscolumnnames.

ALLUPPERCAPSISNOTBETTERBECAUSEITFEELSLIKESOMEONEISSCREAMINGATME.

CamelCaseIsMarginallyBetterButItStillTakesTimeToParse.    

i_recommend_sticking_with_lower_case_and_underscore

通常,最好为“映射表”命名以匹配其描述的关系,而不是被引用表的名称。用户可以有任意数量的关系,产品:user_likes_productuser_bought_productuser_wants_to_buy_product


5
我更喜欢看下划线。但我更喜欢键入camelCase。下划线有些问题……无论我怎么练习,我都不得不停下来看看键盘。
Tydus勋爵,11年

@Ronnis,请您详细说明“”,不要将所有键都命名为“ ID”。指向同一事物的键在所有表中应具有相同的名称。
特拉维斯

@Travis,当然可以,但是整个段落都是详尽的?
罗尼斯(Ronnis)

我想我的问题是关于命名(非区分角色)合成主键的好处 {table_name}_id而不是仅仅id,因为该列将永远仅以表名作为限定符(例如)来引用table_name.id对于上下文,我在一个table_a JOIN table_b ON table_b_id_column不支持表单的联接语法的生态系统中运行;我必须做table_a JOIN table_b ON table_b.id_column = table_a.table_b_id_column
特拉维斯

对我来说,这与清晰度和逻辑数据模型有关。如果我对USER_ID和COMPANY_ID使用数字序列,那么其中一些值当然是相同的。但是USER_ID中的123与COMPANY_ID中的123不同,因为它们的值来自不同的domains。这样,以不同的方式命名它们是有意义的。
罗尼斯

16

单数与复数之间没有“正确”关系,这主要取决于口味。

这部分取决于您的关注点。如果将表视为一个单元,则表包含“复数”(因为它包含许多行-因此,复数名称是适当的)。如果您将表名视为标识表中的一行,则更喜欢使用“单数”。这意味着您的SQL将被视为在表的一行上工作。没关系,尽管通常是过分简化;SQL适用于集合(或多或少)。但是,我们可以单数形式获得该问题的答案。

  1. 由于您可能需要一个表“ user”,另一个“产品”和第三个表来将用户连接到产品,因此您需要一个表“ user_product”。

  2. 由于说明适用于产品,因此应使用“ product_description”。除非每个用户都为自己命名每个产品...

  3. “ user_product”表是(或可以是)具有产品ID和用户ID的表的示例,仅此而已。您可以使用相同的通用方式命名两个属性表:“ user_stuff”。装饰性前缀(例如“ rel_”)并没有帮助。例如,您会看到一些人在每个表名的前面使用“ t_”。那没有什么帮助。


当您说“和第三个连接用户”时。你是说第三张桌子吗?具有一对多关系(用户有很多产品)时,为什么需要第三张表?您是否建议使用user_product代替UserProduct?
Andreas

我的回答是基于存在一个列出系统知道的产品的表的。还应该有一个表,列出系统知道的用户。而且由于一个用户可以(根据我的假设)与一个特定产品相关联,因此存在第三个表,可以将其命名为“ user_product”(或“ product_user”)。如果您实际上只有两个表,那么每个用户的产品都是该用户唯一的,并且从未被其他任何人使用过,那么(a)您有一个不寻常的情况,并且(b)您只需要两个表-您不需要我假设的“产品”表。
乔纳森·莱夫勒

抱歉,我应该使用比产品更好的例子。我的意思是产品对用户而言是独一无二的。因此,清除此内容后,我认为描述表应该为“ user_product_description”,因为它对于用户/产品也是唯一的。.我知道我对产品采取了一个可怕的例子:)谢谢
Andreas

@Andreas:通常很难选择好的例子,问题之一是人们对产品表将包含什么的先入之见。但是,经过您的澄清,然后将“ user”,“ user_product”和“ user_product_description”用作表名。
乔纳森·莱夫勒

4

只要始终使用复数就不错,但我更喜欢单数。

除非您要概述多对多关系,否则我将不使用下划线。并使用初始资本,因为它有助于区分ORM中的事物。

但是有许多命名约定,因此,如果您要使用下划线,则只要保持一致就可以。

所以:

User

UserProduct (it is a users products after all)

如果只有一个用户可以拥有任何产品,则

UserProductDescription

但是,如果产品由用户共享:

ProductDescription

如果您将下划线保存为多对多关系,则可以执行以下操作:

UserProduct_Stuff

在UserProduct和Stuff之间形成M对M的关系-从问题中不确定是否需要多对多的确切性质。


我喜欢这个,似乎是个好方法。我唯一想知道的是,由于我“应该”将下划线保存为多对多,因此我“必须”使用表的大写命名。我不知道为什么,但是我不知道为什么不应该将其用于表名,而只能用于列...我可能是从同一个人那里听说它的意思是复数是错误的。
Andreas,0:

@Andreas您不需要在表中使用大写字母,只需将不同单词的首字母大写即可。
amelvin 2011年

2

使用单数形式比复数形式更正确,您在哪里听说过?我宁愿说复数形式在命名数据库表时更常见……在我看来,逻辑也更多。该表通常包含多于一行;)在概念模型中,尽管实体的名称通常为单数。

关于您的问题,如果“产品”和“产品描述”是模型中带有标识(即实体)的概念,我将简单地称为表“产品”和“产品描述”。对于用于实现多对多关系的表,我通常使用命名约定“ SideA2SideB”,例如“ Student2Course”。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.