Elixir Ecto: 使用Whatwasit追蹤模型的變動和版本化

Whatwasit 是一個跟蹤Ecto模型變化的一個包, 用於審計和版本化. 審計在某些狀況下是咱們很是須要的, 好比咱們須要知道誰在系統中修改了什麼, 能夠造成審計日誌備後期進行審查.html

注意: Whatwasit(讀做: What was it) 須要Elixir 1.2的支持, 因此要使用 Whatwasit 請首先升級到Elixir 1.2以上前端

跟蹤變化

使用 Whatwasit 很簡單, 只須要添加在模型中添加兩行代碼便可, 下面咱們來細說這個過程. 首先建立一個項目:git

這裏咱們只是測試如何使用Whatwasit, 因此去掉前端庫Brunch(--no-brunch).github

mix phoenix.new whatwasit_example --no-brunch

mix.exs中增長依賴:web

defp deps do
  [
    ...
    {:whatwasit, "~> 0.2.1"}
  ]
end

切換到命令行執行數據庫

mix deps.get && mix compile

運行下面的用於建立存儲版本和變動的模型和數據庫遷移腳本segmentfault

➜  whatwasit_example mix whatwasit.install
* creating priv/repo/migrations/20160801031533_create_whatwasit_version.exs
* creating web/models/whatwasit/version.ex
Add the following to your config/config.exs:

  config :whatwasit,
    repo: WhatwasitExample.Repo

Update your models like this:

  defmodule WhatwasitExample.Post do
    use WhatwasitExample.Web, :model
    use Whatwasit         # add this

    schema "posts" do
      field :title, :string
      field :body, :string
      timestamps
    end

    def changeset(model, params \ %{}) do
      model
      |> cast(params, ~w(title body))
      |> validate_required(~w(title body)a)
      |> prepare_version     # add this
    end
  end

執行數據庫遷移腳本瀏覽器

mix ecto.migrate

命令行提示你在模塊頭部添加 use Whatwasit, 在changeset/2 方法的管道尾部添加 prepare_version函數追蹤數據庫的變動. 版本存儲在 versions 表裏面, 其結構以下:session

圖片描述

圖中的 object 字段是一個JSON數據, 存儲了修改以前的快照版本. Whatwasit.Version 模型的定義以下:app

schema "versions" do
  field :item_type, :string
  field :item_id, :integer
  field :action, :string  # ~w(update delete)
  field :object, :map     # versioned schema stored as a map
  
  timestamps
end

其對應的數據庫遷移腳本以下:

defmodule WhatwasitExample.Repo.Migrations.CreateWhatwasitVersion do
  use Ecto.Migration
  def change do
    create table(:versions) do
      add :item_type, :string, null: false
      add :item_id, :integer, null: false
      add :action, :string
      add :object, :map, null: false
      timestamps
    end

  end
end

下面是一個博客的示例

defmodule WhatwasitExample.Post do
  use WhatwasitExample.Web, :model
  use Whatwasit
  alias WhatwasitExample.Repo

  schema "posts" do
    field :title, :string
    field :body, :string

    timestamps
  end

  def getbypk(id) do
    Repo.get(__MODULE__, id)
  end

  def updatebypk(changeset, changes) when is_map(changes) do
    changeset = changeset |> Ecto.Changeset.change(changes)
    changeset |> Repo.update
  end

  def user_changeset(struct, params \\ %{}) do
    # cast/3 把瀏覽器POST過來的數據強制轉換爲schema中定義的數據類型
    # validate_required/3 驗證要求的字段, message選項爲錯誤提示
    struct
    |> cast(params, [:title, :body])
    |> validate_required([:title, :body], [message: "標題和內容是必須的"])
    |> prepare_version
  end

  # def insert(map) do
  #   Map.merge(%__MODULE__{}, map) |> Repo.insert
  # end

  def insert(params) do
    changeset = user_changeset(%__MODULE__{}, params)
    if changeset.valid? do
      Repo.insert(changeset)
    else
      raise "Changeset is invalid."
    end
  end

  @doc """
  更新一條記錄
  """
  def update(params) do
    # 從數據庫獲取一個 %WhatwasitExample.Post{} 結構
    struct = getbypk(params[:id])
    case struct do
      nil ->
        raise "Record not exists."
      struct ->
        fields = Map.delete(params, :id)
        # 經過瀏覽器傳過來的POST數據建立一個Ecto.Changeset
        changeset = user_changeset(struct, fields)
        if changeset.valid? do
          changeset |> Repo.update
        else
          raise "Changeset is invalid when update."
        end
    end
  end

  @doc """
  按主鍵ID刪除一條記錄
  """
  @spec delete(map) :: Ecto.Schema.t | :no_return
  def delete(%{"id" => id}) do
    # 從數據庫獲取一個 %WhatwasitExample.Post{} 結構
    # 從 %WhatwasitExample.Post{} 建立一個 Ecto.Changeset
    # 把這個 Ecto.Changeset 傳遞給 Ecto.Repo.delete!/2
    Repo.get!(__MODULE__, id)
    |> __MODULE__.user_changeset
    |> Repo.delete!
  end
end

圖片描述

跟蹤誰修改了數據

首先須要添加 {:coherence, "~> 0.2.0"} 依賴, Coherence 是一個用戶管理包, 提供了用戶系統的經常使用功能, 包括:

  • 註冊, 註冊新用戶

  • 郵件激活, 生成郵件激活連接經過郵件發送給用戶

  • 密碼恢復, 生成密碼找回鏈接經過郵件發送給用戶

  • 登陸跟蹤, 爲每一個保存了每次登陸的時間, 次數, IP地址

  • 鎖定, 登陸N次錯誤後自動鎖定用戶一段時間

  • 解鎖, 生成一個解鎖鏈接經過郵件發送給用戶

初始化 Coherence

mix coherence.install --full-invitable

上述命令會執行以下步驟:

  • 添加 coherence 的配置到 config/config.exs 文件的尾部.

  • 若是用戶模型不存在, 添加新的用戶模型

  • 添加數據庫遷移腳本文件

    • timestamp_add_coherence_to_user.exs 若是用戶模型已經存在

    • timestamp_create_coherence_user.exs 若是用戶模型不存在

    • timestamp_create_coherence_invitable.exs
      web/views/coherence/ 中添加相關的視圖

web/templates/coherence 添加相關的模板
web/emails/coherence 中添加電子郵件模板
添加 web/coherence_web.ex 文件

最後查看一下 config/config.exs 文件編輯電子郵件的Key, 這裏你能夠申請一個 mailgun 的郵件服務key用於測試.

完整的命令輸出

➜ whatwasit_example# mix coherence.install --full-invitable
Your config/config.exs file was updated.
Compiling 14 files (.ex)
warning: unused import Ecto
  web/models/whatwasit/version.ex:7

Generated whatwasit_example app
* creating priv/repo/migrations/20160801060750_create_coherence_user.exs
* creating web/models/coherence/user.ex
* creating priv/repo/migrations/20160801060751_create_coherence_invitable.exs
* creating web/coherence_web.ex
* creating web/views/coherence/coherence_view.ex
* creating web/views/coherence/email_view.ex
* creating web/views/coherence/invitation_view.ex
* creating web/views/coherence/layout_view.ex
* creating web/views/coherence/coherence_view_helpers.ex
* creating web/views/coherence/password_view.ex
* creating web/views/coherence/registration_view.ex
* creating web/views/coherence/session_view.ex
* creating web/views/coherence/unlock_view.ex
* creating web/templates/coherence/email/confirmation.html.eex
* creating web/templates/coherence/email/invitation.html.eex
* creating web/templates/coherence/email/password.html.eex
* creating web/templates/coherence/email/unlock.html.eex
* creating web/templates/coherence/invitation/edit.html.eex
* creating web/templates/coherence/invitation/new.html.eex
* creating web/templates/coherence/layout/app.html.eex
* creating web/templates/coherence/layout/email.html.eex
* creating web/templates/coherence/password/edit.html.eex
* creating web/templates/coherence/password/new.html.eex
* creating web/templates/coherence/registration/new.html.eex
* creating web/templates/coherence/session/new.html.eex
* creating web/templates/coherence/unlock/new.html.eex
* creating web/emails/coherence/coherence_mailer.ex
* creating web/emails/coherence/user_email.ex

Add the following to your router.ex file.

defmodule WhatwasitExample.Router do
  use WhatwasitExample.Web, :router
  use Coherence.Router         # Add this

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug Coherence.Authentication.Session, login: true  # Add this
  end

  pipeline :public do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug Coherence.Authentication.Session               # Add this
  end

  # Add this block
  scope "/" do
    pipe_through :public
    coherence_routes :public
  end

  # Add this block
  scope "/" do
    pipe_through :browser
    coherence_routes :private
  end

  scope "/", WhatwasitExample do
    pipe_through :public
    get "/", PageController, :index
  end

  scope "/", WhatwasitExample do
    pipe_through :browser
    # Add your protected routes here
  end
end


You might want to add the following to your priv/repo/seeds.exs file.

WhatwasitExample.Repo.delete_all WhatwasitExample.User

WhatwasitExample.User.changeset(%WhatwasitExample.User{}, %{name: "Test User", email: "testuser@example.com", password: "secret", password_confirmation: "secret"})
|> WhatwasitExample.Repo.insert!

Don't forget to run the new migrations and seeds with:
    $ mix ecto.setup

刪除以前生成的數據庫遷移腳本

rm priv/repo/migrations/20160801064451_create_whatwasit_version.exs

運行以下命令, 從新生成模型和數據庫遷移腳本

whatwasit.install --whodoneit

從新建立數據庫

mix ecto.reset

|> prepare_version 修改成 |> prepare_version(opts), 傳入 opts 參數.

修改後的 Post 模型的 changeset 函數, 增長第三個參數 opts:

def user_changeset(struct, params \\ %{}, opts \\ %{}) do
  # cast/3 把瀏覽器POST過來的數據強制轉換爲schema中定義的數據類型
  # validate_required/3 驗證要求的字段, message選項爲錯誤提示
  struct
  |> cast(params, [:title, :body])
  |> validate_required([:title, :body], [message: "標題和內容是必須的"])
  |> prepare_version(opts)
end

上面的多個手工步驟能夠用 mix phoenix.gen.html Post posts title:string body:string 自動生成控制器, 視圖, 模型, 模板文件. 而後修改, 能夠少些不少代碼.

上述步驟都完成後, 能夠經過命令 mix phoenix.routes 查看全部的HTTP端點

➜  whatwasit_example mix phoenix.routes
     session_path  GET     /sessions/new            Coherence.SessionController :new
     session_path  POST    /sessions                Coherence.SessionController :create
registration_path  GET     /registrations/:id/edit  Coherence.RegistrationController :edit
registration_path  GET     /registrations/new       Coherence.RegistrationController :new
registration_path  POST    /registrations           Coherence.RegistrationController :create
registration_path  PATCH   /registrations/:id       Coherence.RegistrationController :update
                   PUT     /registrations/:id       Coherence.RegistrationController :update
registration_path  DELETE  /registrations/:id       Coherence.RegistrationController :delete
    password_path  GET     /passwords/:id/edit      Coherence.PasswordController :edit
    password_path  GET     /passwords/new           Coherence.PasswordController :new
    password_path  POST    /passwords               Coherence.PasswordController :create
    password_path  PATCH   /passwords/:id           Coherence.PasswordController :update
                   PUT     /passwords/:id           Coherence.PasswordController :update
    password_path  DELETE  /passwords/:id           Coherence.PasswordController :delete
      unlock_path  GET     /unlocks/:id/edit        Coherence.UnlockController :edit
      unlock_path  GET     /unlocks/new             Coherence.UnlockController :new
      unlock_path  POST    /unlocks                 Coherence.UnlockController :create
  invitation_path  GET     /invitations/:id/edit    Coherence.InvitationController :edit
  invitation_path  GET     /invitations/new         Coherence.InvitationController :new
  invitation_path  POST    /invitations             Coherence.InvitationController :create
  invitation_path  POST    /invitations/create      Coherence.InvitationController :create_user
  invitation_path  GET     /invitations/:id/resend  Coherence.InvitationController :resend
     session_path  DELETE  /sessions/:id            Coherence.SessionController :delete
        page_path  GET     /                        WhatwasitExample.PageController :index
        post_path  GET     /posts                   WhatwasitExample.PostController :index
        post_path  GET     /posts/:id/edit          WhatwasitExample.PostController :edit
        post_path  GET     /posts/new               WhatwasitExample.PostController :new
        post_path  GET     /posts/:id               WhatwasitExample.PostController :show
        post_path  POST    /posts                   WhatwasitExample.PostController :create
        post_path  PATCH   /posts/:id               WhatwasitExample.PostController :update
                   PUT     /posts/:id               WhatwasitExample.PostController :update
        post_path  DELETE  /posts/:id               WhatwasitExample.PostController :delete

這樣一個基本的帶用戶註冊, 密碼找回, 用戶激活, 等功能的應用程序的基本結構就完成了. 在此基礎之上能夠擴展功能實現更加完整的Web應用程序.

注意事項

  • 目前模型裏面的changeset須要重命名, PROJECT_NAME.Whatwasit.Version模塊中的changeset函數和經過mix phoenix.gen.html 生成的模型中的changeset 函數名稱衝突, 建議修改模型中的changeset函數爲post_changeset

  • mix whatwasit.install --whodoneit-mapmix whatwasit.install --whodoneit區別是, mix whatwasit.install --whodoneit 在versions表中用兩個字段分別存儲用戶名稱和用戶ID, 這是對users表的引用, mix whatwasit.install --whodoneit-map, 用一個字段whodoneit存儲的是一個除密碼以外的用戶全部信息的一個JSON對象. 後者不依賴於用戶信息的變動.

  • 若是用戶模型的主鍵類型爲UUID, 可使用 mix whatwasit.install --whodoneit-id-type=uuid

最後咱們在瀏覽器中輸入 http://127.0.0.1:4000/posts/new 建立一條記錄, 並編輯, 編輯的日誌輸出爲.

這裏是這篇文章的倉庫地址: https://github.com/developerw...

圖片描述

相關文章
相關標籤/搜索