教程:如何建立一对多关系

在本教程的最后,您将了解如何实现并处理一对多和一对一的关系✨!

您可以在github上找到本教程的 结果


指数

1.创建并生成一个新项目
2.一对多/一对一/多对多
3.建筑模型:Pokemon(与用户有关)
4.调整模型:用户(定义与口袋妖怪的关系)
5.建筑控制器:PokemonController
6.调整控制器:UserController(删除相关的宠物小精灵)
7.调整视图:列出用户的所有宠物小精灵
8.调整视图:实现一个表单来创建新的口袋妖怪
9.从这里去哪里


1.创建并生成一个新项目

我们将使用上述教程的结果作为模板来创建我们的新项目:

 蒸气新项目名称--template = vaporberlin / my-first-crud-using-leaf 

在生成Xcode项目之前,我们必须在Package.swift中更改包名称

  // swift-tools-version:4.0 
导入PackageDescriptionlet包=包(
名称:“ projectName ”,
依赖项:[
.package(URL:“ https://github.com/vapor/vapor.git”,来自:“ 3.0.0”),
.package(URL:“ https://github.com/vapor/leaf.git”,来自:“ 3.0.0-rc”),
.package(网址:“ https://github.com/vapor/fluent-sqlite.git”,来自“ 3.0.0-rc”)
],
目标:[
.target(name:“ App”,依赖项:[“ Vapor”,“ Leaf”,“ FluentSQLite”]),
.target(name:“ Run”,依赖项:[“ App”]),
.testTarget(name:“ AppTests”,依赖项:[“ App”]),
]

现在在终端的根目录projectName /上执行:

 蒸气更新-y 

可能需要一些时间来获取依赖关系,但是完成后,您应该具有以下项目结构:

  项目名/ 
├──Package.swift
├──资料来源/
│├──App /
││├──控制器/
│││└──UserController.swift
││├──型号/
│││└──User.swift
││├──app.swift
││├──boot.swift
││├──configure.swift
││└──route.swift
│└──运行/
│└──main.swift
├──测试/
├──资源/
│└──意见/
│└──
├──公众/
├──依存关系/
└──产品/

2.一对多/一对一/多对多

在建立具有关系的模型之前的第一步,我们必须弄清楚我们需要什么样的关系。 我们是需要多对多关系还是一对多关系。 这是为什么? 哦,很高兴你问! 因为如果我们有一个多对多关系,我们将需要三个数据库表,而对于一对多,我们只需要两个。 那个怎么样? 好吧,我看到你很敏锐。 这是因为,例如,在一对多关系中,我们有一个用户(神奇宝贝训练师)可以拥有一个以上的神奇宝贝。 但是像迷惑人一样的神奇宝贝不能由多个用户(神奇宝贝训练师)拥有。 每个宠物小精灵都有一个user_id,因此可以使用此id从User-Table中获取合适的用户。

注意:如果您现在想知道某个用户拥有多少只神奇宝贝,则可以在神奇宝贝表中简单搜索以查找用户ID。

简而言之:如果您具有一对一关系,则它是相同的数据库结构。 两张桌子。 您需要做的就是在用户去之前检查用户是否拥有神奇宝贝大地,并为他创建一个新的口袋妖怪。 如果他已经拥有一个,则您将不允许您的代码进行另一个创建。 那将以一对一结束

因此,为了完整起见,这是多对多的样子:

在这里,您将有一个用户表,一个类表和一个关系表。 如您所见,就像五月一样,用户可以参加许多课程。 她上月亮和木头课。 因此,关系表为她提供了两个条目。 一个指向ID为1的类,另一个条目指向ID为2的类。您还可以看到一个类可以有许多用户。 您可以看到在关系表上,我们有两个用户( user_id:1、2 )引用了class_id:1 (月球)。


3.建筑模型:Pokemon(与用户有关)

Models /中创建一个新的swift文件,并将其命名为Pokemon.swift

注意:我使用终端: 触摸Sources / App / Models / Pokemon.swift

为了让Xcode 看到您的新目录,您可能必须使用蒸气xcode -y重新生成Xcode项目。

Models / Pokemon.swift中,包含以下代码:

  导入FluentSQLite 
导入蒸气 最终类Pokemon:SQLiteModel {
var id:Int?
变量名称:字符串
var级别:字符串 init(
id:整数? =无,
名称:字符串,
级别:字符串
){
self.id = id
self.name =名称
self.level =等级
}
} 扩展神奇宝贝:迁移{}

现在,这是一个与用户无关的简单模型。 让我们添加关系:

 导入FluentSQLite 
导入Vaporfinal类Pokemon:SQLiteModel {
var id:Int?
变量名称:字符串
变量级别:整数
var userID:User.ID init(
id:整数? =无,
名称:字符串,
级别:整数
userID:User.ID
){
self.id = id
self.name =名称
self.level =等级
self.userID =用户ID
}
}扩展用户:内容{}
扩展用户:迁移{}
extension用户:参数{} extension Pokemon {
var user:Parent {
返回父级(\ .userID)
}
}

您真的可以相信这么简单吗? 我们真正需要的只是添加一个属性来存储用户ID,并添加另一个具有关系的属性。

重要的部分是使用Parent 结构定义的关系其中我们的Pokemon类是子级,而User类是父级。 现在访问属性(这里称为user )将返回可以通过提供userID密钥路径找到的父级。 Fluent现在知道该关系所需的一切。 它知道两个表:Pokemon和User。 而从Pokemon中,我们可以获取其父代的值在属性userID后面。

这意味着,每当我们有一个神奇宝贝实例时,我们总是可以通过访问user属性来获取它的父实例。

现在,将我们的Pokemon模型添加到configure.swift中的迁移中:

 进口蒸气 
进口叶
导入FluentSQLitepublic func configure(
_ config:inout Config,
_ env:inout环境,
_服务:inout服务
)抛出{let router = EngineRouter.default()
尝试路线(路由器)
services.register(router,as:Router.self)let leafProvider = LeafProvider()
尝试services.register(leafProvider)
尝试services.register(FluentSQLiteProvider())config.prefer(LeafRenderer.self,for:ViewRenderer.self)var数据库= DatabasesConfig()
尝试database.add(
数据库:SQLiteDatabase(存储:.memory),
如:.sqlite

services.register(数据库)var migrations = MigrationConfig()
migrations.add(模型:User.self,数据库:.sqlite)
migrations.add(模型:Pokemon.self,数据库:.sqlite)
services.register(迁移)
}

如果您现在使用cmd + r运行该命令,则所有内容均应正确构建😊。


4.调整模型:用户(定义与口袋妖怪的关系)

现在转到第二部分:定义用户与其口袋妖怪之间的关系。 让我们转到Models / User.swift为其添加一个Child关系,以便我们也能够获取用户的所有宠物小精灵

 导入FluentSQLite 
导入Vaporfinal类用户:SQLiteModel {
var id:Int?
var用户名:Stringinit(id:Int?= nil,用户名:String){
self.id = id
self.username =用户名
}
}扩展用户:内容{}
扩展用户:迁移{}
扩展名用户:参数{} 扩展名用户{
var pokemons:Children {
返回孩子(\ .userID)
}
}

5.建筑控制器:PokemonController

我们的目标是能够为用户创建口袋妖怪。 因此,在我们的Controllers /目录中创建一个新文件,并将其命名为PokemonController.swift。

注意:我使用终端,只需执行:
触摸Sources / App / Controllers / PokemonController.swift

为了让Xcode 看到您的新文件,您可能必须使用蒸气xcode -y重新生成Xcode项目。

我们将在Controllers / PokemonController.swift中编写一个create函数:

  导入蒸气 最终类PokemonController { func create(_ req:Request) throws- > Future  { 
返回尝试req.content
.decode(Pokemon.PokemonForm.self)
.flatMap {pokemonForm 返回用户
.find(pokemonForm.userId,on:req)
.flatMap { 保护后的 用户 让userId =尝试使用用户?.requireID()否则{
抛出Abort(.badRequest)
} 让宠物小精灵=宠物小精灵(
名称:pokemonForm.name,
级别:pokemonForm.level,
userID:userId
return pokemon.save(on:req).map {_ in
返回req.redirect(收件人:“ /用户”)
}
}
}
}
}

您一定已经注意到,我将Pokemon.PokemonForm.self用于解码内容。 我添加了一个结构,该结构将使其属性完全匹配我们将在几分钟内用于创建新的神奇宝贝的表单的字段。 这已成为社区中的最佳实践。 在Pokemon.swift中添加:

 导入FluentSQLite 
导入Vaporfinal类Pokemon:SQLiteModel {
var id:Int?
变量名称:字符串
变量级别:整数
var userID:User.ID init(
id:整数? =无,
名称:字符串,
级别:整数
userID:User.ID
){
self.id = id
self.name =名称
self.level =等级
self.userID =用户ID
} struct PokemonForm:内容{
变量名称:字符串
变量级别:整数
var userId:Int
}
}扩展神奇宝贝:迁移{}扩展神奇宝贝{
var user:Parent {
返回parent(\。userID)
}
}

现在,让我们在routes.swift中为新的create函数添加一条新路由:

 导入Vaporpublic功能路径(_路由器:路由器)抛出{let userController = UserController() 
router.get(“ users”,使用:userController.list)
router.post(“ users”,使用:userController.create)
router.post(“ users”,User.parameter,“ update”,使用:userController.update)
router.post(“ users”,User.parameter,“ delete”,use:userController.delete) let pokemonController = PokemonController()
router.post(“ pokemon”,使用:pokemonController.create)
}

6.调整控制器:UserController(删除相关的宠物小精灵)

既然我们已经能够为用户创建新的宠物小精灵,我们在删除用户时也需要考虑它们。 因此,在我们的Controller / UserController.swift中调整以下行:

 导入Vaporfinal类UserController {函数列表(_ req:Request)抛出-> Future  { 
...
} func create(_ req:Request)抛出-> Future {
...
} func update(_ req:Request)抛出-> Future {
...
} func delete(_ req:Request)抛出-> Future {
返回尝试req.parameters.next(User.self).flatMap {
返回尝试user.pokemons.query(on:req).delete()。flatMap {_ in
返回user.delete(on:req).map {_ in
返回req.redirect(收件人:“ /用户”)
}
}
}
}
} struct UserForm:内容{
var用户名:字符串
}

7.调整视图:列出用户的所有口袋妖怪

我们将首先调整视图,以同时在Resources / Views / crud.leaf中打印用户的所有口袋妖怪

注意:选择 crud.leaf 并转到 编辑器>语法着色> HTML😉

   


CRUD



CRUD



创建


...

阅读


#for(用户列表中的用户){




#for(user.pokemons中的神奇宝贝){
  • #(pokemon.name)
    }



  • }

    更新


    ...

    删除


    ...



    现在,这看起来和感觉像是可行的,但不会成功。 原因是因为用户仅对存储在属性中的相关口袋妖怪有查询,而对口袋妖怪实例本身没有查询。 现在您会得到:

      {“ error”:true,“ reason”:“无法将迭代器数据转换为数组。(/Path/To/yourProject/Resources\/Views\/crud.leaf行:28列:33范围:1211 .. <1268 )“} 

    我们现在要做的是在将用户和宠物小精灵都传递到视图之前,先获取相关的宠物小精灵。 最好甚至将它们包装在自己的结构中:

    如果看到它,这将很有意义,因此在Controllers / UserController.swift中添加:

     导入Vaporfinal类UserController {函数列表(_ req:Request)抛出-> Future  { 
    让allUsers = User.query(on:req).all()
    return allUsers.flatMap {用户在 let userViewList =尝试users.map {用户在
    返回UserView(
    用户:用户,
    宠物小精灵:试试user.pokemons.query(on:req).all()

    } let data = [“” userViewlist“:userViewList]
    返回尝试req.view()。render(“ crud”,data)
    }
    } func create(_ req:Request)抛出-> Future {
    ...
    } func update(_ req:Request)抛出-> Future {
    ...
    } func delete(_ req:Request)抛出-> Future {
    ...
    }
    } struct UserForm:内容{
    var用户名:字符串
    } struct UserView:可编码{
    var user:用户
    var pokemons:Future
    }

    我们创建了一个符合Encodable的结构。 该结构将容纳一个用户和该用户的口袋妖怪阵列。 这是一个Future数组,但这根本不会打扰我们。 您甚至都不会注意到它,您会看到😊。 好的,在我们的函数中,我们真正要做的就是将每个用户映射到UserView实例。 在地图内部,我们将用户和已执行的神奇宝贝查询的结果传递到新的UserView实例中。

    好的,让我们现在调整curd.leaf文件以处理新的数据结构:

       


    CRUD



    CRUD



    创建


    ...

    阅读


    #for(userViewlist中的userView){




    #for(userView.pokemons中的神奇宝贝){
  • #(pokemon.name)
    }



  • }

    更新


    #for(userViewlist中的userView) {
    <form method =“ POST” action =“ / users /#(userView.user.id)/ update”>

    <input type =“ text” name =“ username” class =“ form-control” value =“ #(userView.user.username) ”>





    }

    删除


    #for(userViewlist中的userView){
    <form method =“ POST” action =“ users /#(userView.user.id)/ delete”>

    <input type =“ text” name =“ username” class =“ form-control” value =“ #(userView.user.username) ”已禁用>





    }



    我们只确保在UserView包装器内访问用户,并且确保在UserView包装器内访问口袋妖怪。 如果现在运行并尝试创建新用户,它将按预期运行。 但是我们还没有看到任何宠物小精灵。 让我们添加一个表单来创建神奇宝贝!


    8.调整视图:实现一个表单来创建新的口袋妖怪

    在下一步中,我们将创建一个表单,其中首先有:一个下拉列表,用于选择我们要为其创建口袋妖怪的用户,其次:我们需要一个新口袋妖怪的字段(名称和级别):

       


    CRUD



    CRUD



    创建


    ...

    阅读


    ...

    更新


    ...

    删除


    ...

    Pokemon








    用户



    #for(userViewlist中的userView){
    #(userView.user.username)
    }


    名称




    级别





    让我解释一下我们在这里所做的事情-有些事情真的很重要! 我假设您已经熟悉如何使用Leaf编写CRUD中的表单标签的工作原理但我们在这里有了新的东西。 我们给了表单标签一个ID,并使用了一个选择标签,在其中我们使用该ID来引用我们的表单。 这非常重要,否则我们的下拉列表中的值将不会发送! 这对我来说是新的-尽管只要它在表单中,它就可以正常工作。 就是输入标签的工作方式。 太天真了。原来,您可以在任何地方使用选择标签! 只要您使用form =” id-of-the-form”来引用表单,那么一旦提交from,这些值就会被发送出去! 👍🏻

    注意:为了从表单旁边的下拉列表(选择标签)发送值,您需要给表单提供一个ID,并使用该ID从选择标签中引用该表单!

    我们最终的cmd + r运行刷新我们的网站 ,就是这样! 您成功实现了一对多关系🎉!!


    9.从这里去哪里

    您可以在Github上找到包含示例项目的所有教程的列表:
    👉🏻https://github.com/vaporberlin/vaporschool


    我很高兴您阅读我的文章! 如果您有任何建议或改进,请告诉我! 我希望收到您的来信! 😊