rtems api用户指南
Elixir代表了相对较新的编程语言,面向更广泛的受众。 它于2011年发布,此后一直在开发中。 他的主要特征是取消功能范式,因为它建立在Erlang之上并在BEAM(Erlang VM)上运行。
Elixir专为构建快速,可扩展和可维护的应用程序而设计,而使用Phoenix可以在Web环境中开发这些应用程序。 Phoenix是用Elixir编写的Web框架,它从流行的框架(例如Python的Django或Ruby on Rails)中汲取了很多概念。 如果您熟悉这些,那将是一个不错的起点。
Elixir / Phoenix是很好的组合,但是在开始编写应用程序之前,那些不熟悉所有概念的人应该首先阅读以下文档。
Elixir随附Mix,它是内置工具,可帮助编译,生成和测试应用程序,获取依赖项等。
我们通过运行来创建我们的应用程序
mix phx.new company_api
这告诉mix创建名为company_api的新Phenix应用。 运行此指令后,将创建应用程序结构:
bash
* creating company_api/config/config.exs
* creating company_api/config/dev.exs
* creating company_api/config/prod.exs
* creating company_api/config/prod.secret.exs
* creating company_api/config/test.exs
* creating company_api/lib / company_api / application . ex
* creating company_api/ lib / company_api . ex
* creating company_api/ lib / company_api_web / channels / user_socket . ex
* creating company_api/ lib / company_api_web / views / error_helpers . ex
* creating company_api/ lib / company_api_web / views / error_view . ex
* creating company_api/ lib / company_api_web / endpoint . ex
* creating company_api/ lib / company_api_web / router . ex
* creating company_api/ lib / company_api_web . ex
* creating company_api/mix.exs
* creating company_api/README.md
* creating company_api/test/support/channel_case.ex
* creating company_api/test/support/conn_case.ex
* creating company_api/test/test_helper.exs
* creating company_api/test/company_api_web/views/error_view_test.exs
* creating company_api/ lib / company_api_web / gettext . ex
* creating company_api/priv/gettext/en/LC_MESSAGES/errors.po
* creating company_api/priv/gettext/errors.pot
* creating company_api/ lib / company_api / repo . ex
* creating company_api/priv/repo/seeds.exs
* creating company_api/test/support/data_case.ex
* creating company_api/ lib / company_api_web / controllers / page_controller . ex
* creating company_api/ lib / company_api_web / templates / layout / app . html . eex
* creating company_api/ lib / company_api_web / templates / page / index . html . eex
* creating company_api/ lib / company_api_web / views / layout_view . ex
* creating company_api/ lib / company_api_web / views / page_view . ex
* creating company_api/test/company_api_web/controllers/page_controller_test.exs
* creating company_api/test/company_api_web/views/layout_view_test.exs
* creating company_api/test/company_api_web/views/page_view_test.exs
* creating company_api/.gitignore
* creating company_api/assets/brunch-config.js
* creating company_api/assets/css/app.css
* creating company_api/assets/css/phoenix.css
* creating company_api/assets/js/app.js
* creating company_api/assets/js/socket.js
* creating company_api/assets/package.json
* creating company_api/assets/static/robots.txt
* creating company_api/assets/static/images/phoenix.png
* creating company_api/assets/static/favicon.ico
如果出现提示,请安装其他依赖项。 接下来,我们需要配置数据库。 在此示例中,我们使用了PostgreSQL,通常Phoenix与该DBMS的集成最佳。
打开/config/dev.exs和/config/test.exs并设置用户名,密码和数据库名称。 设置数据库后,运行
mix ecto .create
这将创建开发和测试数据库,之后
mix phx.server
这应该在默认端口4000上启动服务器(Cowboy)。在浏览器中进行检查,如果看到的是登陆页面,则设置很好。 所有配置都放在/config/config.exs文件中。
在编码之前,将解释开发的几个部分:
请注意,以下部分不会针对整个应用程序进行描述,但是您会明白的。
在开发过程中,我们想编写干净的代码,并且还要考虑规范以及代码在实现之前需要做什么。 这就是为什么我们使用TDD方法。
首先在目录test / company_api_web /中创建模型目录,然后创建user_test.exs。 之后,创建一个模块:
defmodule CompanyApiWeb.UserTest douse CompanyApi.DataCase, async: true
end
在第二行中,我们使用宏用法注入一些外部代码,在这种情况下,将data_case.exs脚本放置在test / support /目录中以及其他脚本中,并使用`async: true`
来表示该测试将与其他测试异步运行。 但是要小心,如果测试将数据写入数据库或在某种意义上更改了某些数据,则它不应运行asyc。
想想应该测试什么。 在这种情况下,让我们测试使用有效和无效数据创建用户。 可以通过模块属性将某些模拟数据设置为常量,例如:
@valid_attributes %{ name: "John" ,subname: "Doe" ,email: "doe@gmail.com" ,job: "engineer"}
当然,您不必使用模块属性,但这可以使代码更简洁。 接下来让我们编写测试。
test"user with valid attributes" douser = CompanyApiWeb.User.reg_changeset(%User{}, @valid_attributes )assert user.valid?
end
在此测试中,我们尝试通过调用方法reg_changeset / 2并声明为真值来创建变更集 。
如果我们用
mixtest test /company_api_web/models/user_test.exs
考试当然会失败。 首先,我们甚至没有用户模块,但是我们甚至没有数据库中的用户表。
接下来,我们需要编写一个迁移。
mix ecto.gen .migration create_user
生成priv / repo / migrations /中的迁移 。 在那里,我们使用Sugar Elixir语法定义了用于表创建的函数,然后将其转换为适当SQL查询。
def change docreate table( :users ) doadd :name , :varcharadd :subname , :varcharadd :email , :varcharadd :job , :varcharadd :password , :varchartimestamps()end
end
函数create / 2从函数table / 2返回的结构中创建数据库表。 有关字段类型,选项和创建索引的详细信息,请阅读docs。 默认情况下,将为每个表生成代理键,名称为id,类型为整数(如果未另外定义)。
现在我们运行命令
mix ecto .migrate
运行迁移。 接下来,我们需要创建模型,因此在lib / company_api_web /中创建models目录,并创建user.ex文件。 我们的模型用于表示数据库表中的数据,因为它将数据映射到Elixir结构中。
defmodule CompanyApiWeb.User douse CompanyApiWeb, :modelschema "users" dofield :name , :stringfield :subname , :stringfield :email , :stringfield :password , :stringfield :job , :stringend def reg_changeset (changeset, params \\ %{}) dochangeset|> cast(params, [ :name , :subname , :email , :job , :password ])|> validate_required([ :name , :subname , :email , :job ])|> validate_format( :email , ~r/\S+@\S+\.\S+ /)end
end
在第2行,我们使用lib / company_api_web / company_api_web.ex中定义的帮助程序,该帮助程序实际上会导入所有必要的模块以创建模型。 如果打开文件,您会看到模型实际上是一个函数,与控制器,视图,通道,路由器等相同。(如果没有模型函数,则可以自己添加)。
两种重要的方法是模式&#xff08;表<->结构映射&#xff09;和changeset / 2 。 Changeset函数不是必需的&#xff0c;但是Elixir的方法就是创建修改数据库的结构。 我们可以定义一个用于注册&#xff0c;登录等。所有验证和关联检查都可以在我们尝试将数据插入数据库之前完成。
有关更多详细信息&#xff0c;请查看Ecto.Changeset文档。 如果我们现在再次运行测试&#xff0c;它将通过。 根据需要添加任意数量的测试用例&#xff0c;并尝试覆盖所有边缘用例。
这应该包含简单模型的创建。 添加关联将在前面提到。
测试控制器与测试模型同等重要。 我们将测试新用户的注册&#xff0c;并让所有注册用户进入系统。 再次&#xff0c;我们在test / company_api_web / controllers /中创建名称为user_controller_test.exs的测试。 通过控制器测试&#xff0c;我们将使用conn_case.exs脚本。 在测试模型时&#xff08;因为我们不需要&#xff09;&#xff0c;测试中没有提到的另一重要事项是设置块。
setupdouser &#61;%User{}|> User.reg_changeset( &#64;user )|> Repo.insert!conn &#61;build_conn()|> put_req_header( "accept" , "application/json" )%{ conn: conn, user: user}
end
在调用每个测试用例之前&#xff0c;将调用Setup块&#xff0c;并且在此块中&#xff0c;我们准备用于测试的数据。 我们可以以元组或映射的形式从块中返回数据。 在此块中&#xff0c;我们将一个用户插入数据库并创建连接结构&#xff08;即连接模型&#xff09;。 同样&#xff0c;常量可用于设置数据。
&#64;valid_data %{ name: "Jim" ,subname: "Doe" ,email: "doe&#64;gmail.com" ,job: "CEO"}
&#64;user %{ name: "John" ,subname: "Doe" ,email: "doe&#64;gmail.com" ,job: "engineer"}
&#64;user_jane %{ name: "Jane" ,subname: "Doe" ,email: "jane&#64;gmail.com" ,job: "architect"}
现在&#xff0c;让我们编写测试&#xff0c;以发送创建新用户的请求。 服务器应处理请求&#xff0c;生成密码&#xff0c;创建新用户&#xff0c;使用生成的密码发送电子邮件并将用户作为json返回。 听起来很多&#xff0c;但我们会慢慢进行。 请注意&#xff0c;您应该尝试涵盖所有“路径”和边缘情况。 首先让我们先测试有效数据&#xff0c;然后再测试无效数据。
describe"tries to create and render" dotest "user with valid data" , %{ conn: conn} doresponse &#61;post(conn, user_path(conn, :create ), user: &#64;valid_data )|> json_response( 201 )assert Repo.get_by(User, name: "Jim" )assert_delivered_email Email.create_mail(response[ "password" ], response[ "email" ])endtest "user with invalid data" , %{ conn: conn} doresponse &#61;post(conn, user_path(conn, :create ), user: %{})|> json_response( 422 )assert response[ "errors" ] !&#61; %{}end
end
每个测试将发布请求发送到特定路径&#xff0c;然后我们检查json响应和断言值。
运行此测试
mixtest test /company_api_web/controller/user_controller_test.exs
会导致错误。 我们没有user_path / 3函数&#xff0c;这意味着未定义路由。 打开lib / company_api_web / router.ex 。 我们将添加范围“ / api”&#xff0c;它将通过&#xff1a;api管道。 我们可以将路由定义为资源&#xff0c;单独或嵌套路由。 定义这样的新资源&#xff1a;
resources "/users", UserController,only : [: index , : create ]
这样&#xff0c;Phoenix将创建路由&#xff0c;这些路由映射到索引并创建函数并由UserController处理。 如果打开控制台并键入
mix phx .routes
您可以看到路线列表&#xff0c;其中有user_path路线&#xff0c;一条路线带动词GET&#xff0c;另一条带动词POST。 现在&#xff0c;如果我们再次运行测试&#xff0c;这一次我们将得到另一个错误&#xff0c;缺少创建函数。 原因是我们没有UserController。 在lib / company_api_web / controllers中添加user_controller.ex。
现在定义新模块&#xff1a;
defmodule CompanyApiWeb.UserController douse CompanyApiWeb, :controller
end
接下来&#xff0c;我们需要创建那个create / 2函数。 Create函数必须接受conn struct&#xff08;并返回它&#xff09;和params。 参数是结构&#xff0c;它承载浏览器提供的所有数据。 我们可以使用Elixir的一项强大功能&#xff0c;即模式匹配&#xff0c;将我们所需的数据与变量进行匹配。
def create (conn, %{ "user" &#61;> user_data}) doparams &#61; Map.put(user_data, "password" , User.generate_password())case Repo.insert(User.reg_changeset(%User{}, params)) do{ :ok , user} ->conn|> put_status( :created )|> render( "create.json" , user: user){ :error , user} ->conn|> put_status( :unprocessable_entity )|> render( "error.json" , user: user)end
end
在我们的测试中&#xff0c;我们通过post方法的params 用户user &#64;valid_data发送该数据将与user_data匹配。 在用户模型中&#xff0c;定义generate_password函数&#xff0c;因此我们可以为每个新用户生成随机密码。
def generate_password do:crypto .strong_rand_bytes( &#64;pass_length )|> Base.encode64|> binary_part( 0 , &#64;pass_length )end
根据需要设置密码的长度。 由于user_data是一个映射&#xff0c;我们将使用键“ password”将新生成的密码放入该映射内。
尽管Elixir具有try / rescue块&#xff0c;但很少使用它们。 通常&#xff0c;大小写和模式匹配的组合用于错误处理。 函数insert&#xff08;注意&#xff0c;我们不会使用insert&#xff01;函数&#xff0c;因为它引发异常&#xff09;返回两个元组之一&#xff1a;
{:ok , Ecto.Schema.t}
{ :error , Ecto.Changeset.t}
基于返回的元组&#xff0c;我们发送适当的响应。 由于我们正在制作JSON API&#xff0c;因此我们应该以json格式返回数据。 从控制器返回的所有数据均由适当的视图处理。 如果我们再次运行测试&#xff0c;将会得到另一个错误。 我们需要做的最后一件事是添加视图文件。 在lib / company_api_web / views /中创建user_view.ex文件&#xff0c;并在其中定义新模块和呈现方法。
defmodule CompanyApiWeb.UserView douse CompanyApiWeb, :viewdef render ( "create.json" , %{ user: user}) dorender_one(user, CompanyApiWeb.UserView, "user.json" )enddef render ( "error.json" , %{ user: user}) do%{ errors: translate_errors(user)}enddef render ( "user.json" , %{ user: user}) do%{ id: user.id, name: user.name, subname: user.subname, password: user.password, email: user.email, job: user.job}enddefp translate_errors (user) doEcto.Changeset.traverse_errors(user, &translate_error/ 1 )end
end
首先从控制器调用render方法&#xff0c;在该方法中我们将调用键&#xff0c;视图模块和模板名称传递给render_one / 3 &#xff0c;因此我们可以对match方法进行模式化。 现在&#xff0c;我们返回将要编码为json的数据。 我们不必调用render_one / 3方法&#xff0c;我们可以立即返回json&#xff0c;但这更加方便。
第二个render方法以json的形式呈现changeset结构提供的错误。 内置方法Ecto.Changeset.traverse_errors / 2从changeet.errors结构中提取错误字符串。
如果我们删除断言表明已发送电子邮件的那一行&#xff0c;则测试将通过。 这样就完善了我们测试和编写控制器的方式。 现在&#xff0c;您可以测试和编写索引方法&#xff0c;并添加涵盖更多代码的更多测试用例。
Elixir中有几个电子邮件库&#xff0c;但是在这个项目中&#xff0c;我们决定使用Bamboo 。 初始设置后&#xff0c;其用法相当简单。 打开mix.exs文件&#xff0c;并在deps函数下添加以下行&#xff1a;
{:bamboo , "~> 0.8" }
然后运行以下命令&#xff1a;
mix deps.get
这将下载依赖项。 之后&#xff0c;在应用程序功能中将Bamboo添加为extra_application。
在全局配置文件中&#xff0c;添加Bamboo的配置&#xff1a;
config:company_api , CompanyApi.Mailer,adapter: Bamboo.LocalAdapter
在这里&#xff0c;我们使用Bamboo.LocalAdapter&#xff0c;但也有其他适配器。 现在&#xff0c;创建模块CompanyApi.Mailer和以下行&#xff1a;
use Bamboo.Mailer, otp_app: :company_api
在使用mailer之前&#xff0c;我们应该定义电子邮件结构。 添加到模型目录中的Email.ex文件&#xff08;请注意&#xff0c;您应该先编写测试文件&#xff0c;然后添加文件&#xff0c;但我们现在将跳过该文件&#xff09;。
defmodule CompanyApiWeb.Email doimport Bamboo.Emaildef create_mail (password, email) donew_email()|> to(email)|> from( "company&#64;gmail.com" )|> subject( "Generated password" )|> html_body( "
Welcome to Chat
" )|> text_body( "Welcome. This is your generated password #{password} . You can change it anytime." )end
end
函数create_mail / 2返回我们将用于发送的电子邮件结构。 在运行测试之前&#xff0c;我们需要在/config/test.exs中添加配置&#xff0c;与以前相同&#xff0c;唯一的区别是适配器&#xff0c;即现在的Bamboo.TestAdapter。 添加这个&#96;use Bamboo.Test&#96;
可以在我们的测试中使用诸如&#96;assert_delivered_email&#96;
的功能。 现在&#xff0c;在UserController中成功插入后&#xff0c;添加下一行&#xff1a;
Email.create_mail(user.password, user.email)
|> CompanyApi.Mailer.deliver_later
这将创建电子邮件结构并在后台发送它。 对于异步发送&#xff0c;有任务模块。 如果您希望查看已发送的邮件&#xff0c;请在router.exs中添加以下内容&#xff1a;
if Mix.env &#61;&#61;:dev doforward "/send_mails" , Bamboo.EmailPreviewPlug
end
现在&#xff0c;我们可以在localhost&#xff1a;4000 / sent_mails看到传递的邮件。
到目前为止&#xff0c;我们已经展示了如何编写测试&#xff0c;迁移&#xff0c;模型&#xff0c;控制器&#xff0c;视图和路由。 更重要的一件事是验证用户身份。 这里选择的图书馆是Guardian 。 它使用JWT&#xff08;Json Web令牌&#xff09;作为身份验证方法&#xff0c;我们可以对Phoenix服务以及通道进行身份验证。 好东西。
首先在mix.exs文件中添加依赖项&#96;{:guardian, "~> 1.0-beta"}&#96;
并运行
mix deps.get
在Guardian文档中&#xff0c;有详细的说明如何设置基本配置&#xff0c;但我们将在此处逐步进行。 打开/config/config.exs并添加以下内容&#xff1a;
config:company_api , CompanyApi.Guardian,issuer: "CompanyApi" ,secret_key: "QDG1lCBdCdjwF49UniOpbxgUINhdyvQDcFQUQam&#43;65O4f9DgWRe09BYMEEDU1i9X" ,verify_issuer: true
请注意&#xff0c;CompanyApi.Guardian将成为我们要创建的模块。 您不必称其为Guardian&#xff0c;也许有点多余。 无论如何&#xff0c;接下来的事情是必须生成的secret_key。 这是一个秘密密钥的示例&#xff0c;可以通过运行来生成
mix guardian.gen .secret
在lib / company_api /中创建CompanyApi.Guardian模块。
defmodule CompanyApi.Guardian douse Guardian, otp_app: :company_apialias CompanyApi.Repoalias CompanyApiWeb.Userdef subject_for_token (user &#61; %User{}, _claims) do{ :ok , "User: #{user.id} " }enddef subject_for_token ( _ ) do{ :error , "Unknown type" }enddef resource_from_claims (claims) doid &#61; Enum.at(String.split(claims[ "sub" ], ":" ), 1 )case Repo.get(User, String.to_integer(id)) donil ->{ :error , "Unknown type" }user ->{ :ok , user}endend
end
创建令牌时将使用此模块。 我们将用户ID作为令牌的主题&#xff0c;这样我们就可以始终从数据库中获取用户。 这可能是最方便的方法&#xff0c;但不是唯一的方法。 我们要做的下一步是建立守护程序管道。 通过插头使用Guardian很容易。 打开lib / company_api_web / router.ex并添加新管道&#xff1a;
pipeline:auth doplug Guardian.Plug.Pipeline, module: CompanyApi.Guardian,error_handler: CompanyApi.GuardianErrorHandlerplug Guardian.Plug.VerifyHeader, realm: "Bearer"plug Guardian.Plug.EnsureAuthenticatedplug Guardian.Plug.LoadResource, ensure: trueend
该管道可以直接在router.ex文件中定义&#xff0c;也可以在单独的模块中定义&#xff0c;但是仍然需要在此处引用。 当用户尝试调用某些服务时&#xff0c;他的请求将通过管道传递。 请注意&#xff0c;此管道专门用于JSON API 。
Okey&#xff0c;首先&#xff0c;我们定义我们正在使用插件管道和引用实现模块以及将要处理auth错误的模块&#xff08;我们将创建它&#xff09;。 下一个插件验证令牌是否在请求标头中&#xff0c;插件确保通过AuthenticAuthenticated确保提供了有效的JWT令牌&#xff0c;最后一个插件通过调用CompanyApi.Guardian模块中指定的函数resource_from_claims / 1来加载资源。
由于缺少auth_error处理模块&#xff0c;请将其添加到lib / company_api /中 。
defmodule CompanyApi.GuardianErrorHandler dodef auth_error (conn, {_type, reason}, _opts) doconn|> Plug.Conn.put_resp_content_type( "application/json" )|> Plug.Conn.send_resp( 401 , Poison.encode!(%{ message: to_string(reason)}))end
end
毒药是Elixir JSON库。 只需在mix.exs中添加依赖项&#96;{:poison, "~> 3.1"}&#96;
。
我们已经为Guardian设置了所有内容&#xff0c;现在是时候编写SessionController并处理登录和注销了。 首先&#xff0c;我们必须编写测试。 创建session_controller_test.exs。 我们将测试用户登录并使其通过。 我们已经为UserController编写了测试&#xff0c;因此您也知道如何设置这一测试。
test"login as user" , %{ conn: conn, user: user} douser_credentials &#61; %{ email: user.email, password: user.password}response &#61;post(conn, session_path(conn, :create ), creds: user_credentials)|> json_response( 200 )expected &#61; %{"id" &#61;> user.id,"name" &#61;> user.name,"subname" &#61;> user.subname,"password" &#61;> user.password,"email" &#61;> user.email,"job" &#61;> user.job}assert response[ "data" ][ "user" ] &#61;&#61; expectedrefute response[ "data" ][ "token" ] &#61;&#61; nilrefute response[ "data" ][ "expire" ] &#61;&#61; nilend
我们将尝试使用有效的凭据登录&#xff0c;并期望以响应用户身份获得令牌和过期值。 如果我们运行此测试&#xff0c;它将失败。 我们没有session_path路由。 打开router.ex文件&#xff0c;并在我们的“ / api”范围内添加新路由&#xff1a;
post"/login" , SessionController, :create
我们将此路由置于“ / api”范围内&#xff0c;因为我们的用户在尝试登录时不需要进行身份验证。 如果我们再次运行测试&#xff0c;这次将失败&#xff0c;因为没有创建功能。
现在添加SessionController并编写登录功能。
def create (conn, %{ "creds" &#61;> params}) donew_params &#61; Map.new(params, fn {k, v} -> {String.to_atom(k), v} end )case User.check_registration(new_params) do{ :ok , user} ->new_conn &#61; Guardian.Plug.sign_in(conn, CompanyApi.Guardian, user)token &#61; Guardian.Plug.current_token(new_conn)claims &#61; Guardian.Plug.current_claims(new_conn)expire &#61; Map.get(claims, "exp" )new_conn|> put_resp_header( "authorization" , "Bearer #{token} " )|> put_status( :ok )|> render( "login.json" , user: user, token: token, exp: expire){ :error , reason} ->conn|> put_status( 401 )|> render( "error.json" , message: reason)endend
第一行以键为原子的结果创建新地图。 函数check_registration / 1检查数据库中是否存在具有给定凭据的用户。 如果用户存在&#xff0c;我们将其登录&#xff0c;创建新令牌并终止日期。 之后&#xff0c;我们设置响应头&#xff0c;状态和渲染用户。 为了渲染&#xff0c;我们需要在lib / company_api_web / views /中创建session_view.ex。
defmodule CompanyApiWeb.SessionView douse CompanyApiWeb, :viewdef render ( "login.json" , %{ user: user, token: token, exp: expire}) do%{data: %{user: render_one(user, CompanyApiWeb.UserView, "user.json" ),token: token,expire: expire}}enddef render ( "error.json" , %{ message: reason}) do%{ data: reason}end
end
现在测试应该通过了。 当然&#xff0c;应该添加更多测试&#xff0c;但这取决于您。 注销非常简单&#xff0c;&#96;Guardian.revoke&#xff08;CompanyApi.Guardian&#xff0c;token&#xff09;&#96;从标头中删除令牌&#xff0c;这就是我们需要做的。 使用API并没有真正的注销&#xff0c;但这是可行的。 在添加新的登出路径之前&#xff0c;我们需要定义“新范围”。 实际上&#xff0c;这将再次成为相同的“ / api”作用域&#xff0c;但是现在它将通过两个管道进行操作&#xff1a; &#96;pipe_through [:api, :auth]&#96;
。
我们为什么这样做呢&#xff1f; 每个需要认证的新路由都将位于此新范围内。 另外&#xff0c;如果要注销&#xff0c;则需要首先进行身份验证。 这样&#xff0c;我们就涵盖了与Guardian进行身份验证的过程。 稍后将提到套接字身份验证&#xff0c;它甚至更容易。
由于这是一个聊天应用程序&#xff0c;因此必须以某种方式保存消息历史记录。 我们将再添加两个实体&#xff0c;它们代表两个用户之间的对话以及用户的消息。 这将是展示Ecto中关联示例的好机会。
我们要添加的第一个实体是对话实体。 对话将同时属于参与聊天的用户&#xff0c;并且该用户将进行许多对话。 对话中还将有许多消息是第二个实体。 消息将属于用户和某些对话。 在这种情况下&#xff0c;用户代表发送消息的人。 消息的其他属性是日期和内容。
我们用几句话描述了我们的数据模型。 这些数据模型中的每一个都有自己的测试&#xff0c;控制器和视图&#xff0c;但是由于我们已经解释了所有这些内容&#xff0c;因此在这一部分中&#xff0c;我们将重点关注这些实体之间的关联。 请注意&#xff0c;您唯一需要做的就是编写用于创建对话&#xff0c;创建消息和获取消息历史记录的功能。
首先&#xff0c;让我们添加对话迁移。
运行命令
mix ecto.gen .migration create_conversations
现在&#xff0c;我们需要使用正确的列创建表对话&#xff1a;
def change docreate table( :conversations ) doadd :sender_id , references( :users , null: false )add :recipient_id , references( :users , null: false )timestamps()endcreate unique_index( :conversations , [ :sender_id , :recipient_id ], name: :sender )end
如您所见&#xff0c;我们正在添加外键sender_id和收件人_id&#xff0c;并且正在引用users表。 这将代表我们两个用户的对话。 两个键不能为null。 我们要做的最后一件事是在与唯一约束相对应的两列上创建unique_index。 我们这样做是因为我们不希望重复的对话具有相同的ID。 现在创建模型&#xff1a;
defmodule CompanyApiWeb.Conversation douse CompanyApiWeb, :modelalias CompanyApiWeb.{User, Message}schema "conversations" dofield :status , :stringbelongs_to :sender , User, foreign_key: :sender_idbelongs_to :recipient , User, foreign_key: :recipient_idhas_many :messages , Messagetimestamps()enddef changeset (changeset, params \\ %{}) dochangeset|> cast(params, [ :sender_id , :recipient_id , :status ])|> validate_required([ :sender_id , :recipient_id ])|> unique_constraint( :sender_id , name: :sender )|> foreign_key_constraint( :sender_id )|> foreign_key_constraint( :recipient_id )end
观察新功能。 函数belongs_to / 3和has_many / 3表示关联。 通常&#xff0c; belongs_to / 3函数是用名称和引用的模块定义的&#xff0c;但是这一次&#xff0c;因为我们对同一模块有两个引用&#xff0c;所以我们必须添加一个对应的外键列。
has_many / 3关联&#xff0c;关联名称和模块也有同样的情况&#xff08;我们将很快创建Message模块&#xff09;。 现在更改集。 我们添加了两个foreign_key_contraint / 3函数&#xff0c;每个外键一个&#xff0c;并且添加了unique_constraint / 3函数&#xff08;由于复合唯一列&#xff0c;只需要指定一个&#xff09;。 所有这些约束都在数据库级别检查。
第二实体是消息。 跑
mix ecto.gen .migration create_messages
添加创建和表功能&#xff1a;
def change docreate table( :messages ) doadd :sender_id , references( :users , null: false )add :conversation_id , references( :conversations , null: false )add :content , :varcharadd :date , :naive_datetimetimestamps()endcreate index( :messages , [ :sender_id ])create index( :messages , [ :conversation_id ])end
和以前一样的故事。 消息属于两个外键&#xff0c;消息分别属于用户&#xff08;发送者&#xff09;和会话。 这次我们不需要唯一的约束&#xff0c;所以我们只索引提到的字段。 看一下模型&#xff1a;
defmodule CompanyApiWeb.Message douse CompanyApiWeb, :modelalias CompanyApiWeb.{User, Conversation}schema "messages" dofield :content , :stringfield :date , :naive_datetimebelongs_to :conversation , Conversationbelongs_to :sender , User, foreign_key: :sender_idtimestamps()enddef changeset (changeset, params \\ %{}) dochangeset|> cast(params, [ :sender_id , :conversation_id , :content , :date ])|> validate_required([ :sender_id , :conversation_id , :content , :date ])|> foreign_key_constraint( :sender_id )|> foreign_key_constraint( :conversation_id )end
我们要做的最后一件事是在“用户”模块中添加关联&#xff1a;
has_many:sender_conversations , Conversation, foreign_key: :sender_idhas_many :recipient_conversations , Conversation, foreign_key: :recipient_idhas_many :messages , Message, foreign_key: :sender_id
这样&#xff0c;我们就建立了数据模型&#xff0c;并且您已经看到了Ecto关联的简要示例。 对于many_to_many关联&#xff0c;请阅读docs 。
本质上&#xff0c;通道是基于套接字顶部的Phoenix抽象。 一个套接字连接上可以有多个通道。 有关详细说明和了解渠道推荐的方式&#xff0c;请阅读官方文档 。
我们的目标是通过Websocket协议发送消息&#xff0c;而我们将从编写通道测试开始。 有关频道测试的文档确实很有帮助。
在/ test / company_api_web / channels /目录中创建chat_room_test.exs。 在设置块中&#xff0c;将一个用户插入数据库&#xff0c;创建连接并登录用户。 我们将测试消息发送。
defmodule CompanyApiWeb.ChatRoomTest douse CompanyApiWeb.ChannelCasealias CompanyApi.Guardian, as: Guardalias CompanyApiWeb.{ChatRoom, UserSocket, Conversation}&#64;first_user_data %{ name: "John" ,subname: "Doe" ,email: "doe&#64;gmail.com" ,job: "engineer"}&#64;second_user_data %{ name: "Jane" ,subname: "Doe" ,email: "jane&#64;gmail.com" ,job: "architect"}setup douser &#61;%User{}|> User.reg_changeset( &#64;first_user_data )|> Repo.insert!{ :ok , token, _claims} &#61; Guard.encode_and_sign(user){ :ok , soc} &#61; connect(UserSocket, %{ "token" &#61;> token}){ :ok , _ , socket} &#61; subscribe_and_join(soc, ChatRoom, "room:chat" ){ :ok , socket: socket, user: user}endtest "checks messaging" , %{ socket: socket, user: u} douser &#61;%User{}|> User.reg_changeset( &#64;second_user_data )|> Repo.insert!conv &#61;%Conversation{}|> Conversation.changeset(%{ sender_id: u.id, recipient_id: user.id})|> Repo.insert!{ :ok , token, _claims} &#61; Guard.encode_and_sign(user){ :ok , soc} &#61; connect(UserSocket, %{ "token" &#61;> token}){ :ok , _ , socketz} &#61; subscribe_and_join(soc, ChatRoom, "room:chat" )push socket, "send_msg" , %{ user: user.id, conv: conv.id, message: "Hi! This is message" }assert_push "receive_msg" , %{ message: message}assert message.content &#61;&#61; "Hi! This is message"refute Repo.get!(CompanyApiWeb.Message, message.id) &#61;&#61; nilpush socketz, "send_msg" , %{ user: u.id, conv: conv.id, message: "This is a reply" }assert_push "receive_msg" , %{ message: reply}assert reply.content &#61;&#61; "This is a reply"refute Repo.get!(CompanyApiWeb.Message, reply.id) &#61;&#61; nilend
end
好吧&#xff0c;这似乎很多&#xff0c;但请逐步进行。 在设置块中&#xff0c;我们使用生成的令牌连接到套接字&#xff0c;然后函数subscribe_and_join / 3将用户加入列出的主题。 在测试之后&#xff0c;对第二个用户重复这些步骤&#xff0c;然后创建对话。 函数push / 3允许我们直接通过套接字发送消息&#xff0c;而assert_push或assert_broadcast声明推送或广播的消息。 运行测试将导致错误。
打开lib / company_api_web / channels / user_socket.ex并定义新频道
channel "room:*" , CompanyApiWeb.ChatRoom
在这里修改connect / 2和id / 1函数。 我们希望使只有经过身份验证的用户才能连接到套接字。
def connect (%{ "token" &#61;> token}, socket) docase Guardian.Phoenix.Socket.authenticate(socket, CompanyApi.Guardian, token) do{ :ok , socket} ->{ :ok , socket}{ :error , _ } ->:errorendenddef connect (_params, _socket), do: :errordef id (socket) douser &#61; Guardian.Phoenix.Socket.current_resource(socket)"user_socket: #{user.id} "end
&#96;Guardian.Phoenix.Socket.authenticate(socket, CompanyApi.Guardian, token)&#96;
提供了身份验证。 函数id / 1返回套接字ID&#xff0c;我们将其设置为用户ID。
现在让我们创建一个新频道。 在同一目录中创建channel_room.ex文件&#xff0c;但现在保留它。 由于我们正在进行私人聊天&#xff0c;因此我们需要知道要向其发送消息的套接字。 有一些方法可以实现这一目标。 这里的决定是将打开的套接字连接存储在映射&#96;{user_id: socket}&#96;
。
Elixir提供了两种用于存储状态的抽象&#xff1a; GenServers和Agent 。 为了理解GenServer或代理文档的概念&#xff0c;必须阅读。
打开lib / company_api /并创建channel_sessions.ex&#xff0c;这将是我们用于存储套接字的GenServer。
defmodule CompanyApi.ChannelSessions douse GenServer#Client sidedef start_link (init_state) doGenServer.start_link(__MODULE_ _ , init_state, name: __MODULE_ _ )enddef save_socket (user_id, socket) doGenServer.call(__MODULE_ _ , { :save_socket , user_id, socket})enddef delete_socket (user_id) doGenServer.call(__MODULE_ _ , { :delete_socket , user_id})enddef get_socket (user_id) doGenServer.call(__MODULE_ _ , { :get_socket , user_id})enddef clear () doGenServer.call(__MODULE_ _ , :clear )end#Server callbacksdef handle_call ({ :save_socket , user_id, socket}, _from, socket_map) docase Map.has_key?(socket_map, user_id) dotrue ->{ :reply , socket_map, socket_map}false ->new_state &#61; Map.put(socket_map, user_id, socket){ :reply , new_state, new_state}endenddef handle_call ({ :delete_socket , user_id}, _from, socket_map) donew_state &#61; Map.delete(socket_map, user_id){ :reply , new_state, new_state}enddef handle_call ({ :get_socket , user_id}, _from, socket_map) dosocket &#61; Map.get(socket_map, user_id){ :reply , socket, socket_map}enddef handle_call ( :clear , _from, state) do{ :reply , %{}, %{}}end
end
GenServer抽象了常见的客户端-服务器交互。 客户端调用服务器端回调。 这些回调对地图进行操作。 该模块应在应用程序启动时启动&#xff0c;因此我们将其添加到Supervision tree中 。 这是Elixir中最美丽的东西之一。
在同一目录中打开application.ex文件&#xff0c;并在子级列表中添加&#96;worker(CompanyApi.ChannelSessions, [%{}])&#96;
这一行。 这将以初始状态&#96;%{}&#96;
在应用程序的开头启动ChannelSessions。 现在我们可以编写ChatRoom频道。 每个通道必须实现两个回调join / 3和handle_in / 3 。
defmodule CompanyApiWeb.ChatRoom douse CompanyApiWeb, :channelalias CompanyApi.{ChannelSessions, ChannelUsers}alias CompanyApiWeb.Messagedef join ( "room:chat" , _payload, socket) douser &#61; Guardian.Phoenix.Socket.current_resource(socket)send( self (), { :after_join , user}){ :ok , socket}enddef handle_in ( "send_msg" , %{ "user" &#61;> id, "conv" &#61;> conv_id, "message" &#61;> content}, socket) docase ChannelSessions.get_socket id donil ->{ :error , socket}socketz ->user &#61; Guardian.Phoenix.Socket.current_resource(socket)case Message.create_message(user.id, conv_id, content) donil ->{ :noreply , socket}message ->push socketz, "receive_msg" , %{ message: message}{ :noreply , socket}endendenddef handle_info ({ :after_join , user}, socket) doChannelSessions.save_socket(user.id, socket){ :noreply , socket}enddef terminate (_msg, socket) douser &#61; Guardian.Phoenix.Socket.current_resource(socket)ChannelSessions.delete_socket user.idend
end
由于我们需要保存套接字&#xff0c;因此只能在创建套接字后才能将其完成&#xff0c;该套接字位于join / 3回调的末尾。 因此&#xff0c;我们向自己发送消息&#xff0c;该消息将调用回调方法handle_info / 2 。 在那里&#xff0c;我们将套接字添加到地图中。 回调handle_in / 3创建一条消息并将其发送给适当的用户。 函数teminate / 2从地图上删除套接字。
设置完成后&#xff0c;聊天应用程序API已完成。 本教程涵盖了早期列出的所有部分以及OTP的一些高级内容&#xff0c;例如GenServer。 它旨在在开发一个Elixir应用程序时显示工作流程&#xff0c;并且为了完全理解需要文档阅读。 毕竟&#xff0c;这里有所有信息。
所有Elixir爱好者的推荐场所&#xff0c; Elixir论坛 。
先前发布在https://kolosek.com/elixir-basic-api-guide/
翻译自: https://hackernoon.com/basic-elixir-api-guide-2h48u3y7z
rtems api用户指南