2021年5月24日 星期一

Building GraphQL pattern with Ecto (I) - Ecto Part

本篇文章是對個人學習 Elixir 之路做的紀錄,學習過程中有點小顛坡,從 2021-01-01 開始正式學習 Elixir 生態,過程中都是看 pragprog 出版社的書居多 (Chris McCord 的那幾本 Phoenix, Meta-programming 和 Craft GraphQL APIs in elixir with absinthe),跟一本歐萊禮的 Introducing Elixir: Getting Started in Functional Programming。

學習障礙主要是我不是買最新 Edition 的書,是舊版的 pdf,舊版的 pdf 所述說的 Phoenix 架構其實有變得比較不太一樣,傳參數的寫法也不同,像是 params \\ :empty 現今也直接用 params \\ %{} 取代了,新版的 Phoenix 那樣跑會直接出錯給你看,還有一些加密套件 comeonin 也不是書上的那種用法了,不過我覺得靠著一些個人的見解跟強勢的經驗法則,還蠻快就克服這些問題,可是對於完全新手來說,其實沒有一個指標性又最新的書可以參考,畢竟我覺得這個生態還蠻缺少知識資源的累積。  但要是對 Elixir 有一個了解之後,其實這些就都不是障礙了,最快速的學習方法是如何盡快的掌握 Elixir,在顯然在舊資源 + Google 的情況下還是可以解決的。

關於這個文章,是 Building GraphQL pattern with Ecto 的上集,會講述建專案、結構到 Ecto 操作資料庫 (Ecto 不是 ORM,他是將資料庫操作框架抽象化成工具,可以通用多個資料庫),而且會寫幾個查詢,讓這些查詢在下集時被使用。

*小提醒: 回查文件 (像是 Ecto),應該是解決問題的好方法,因為 Elixir 生態似乎都是如此,至少官方還有把文件寫得比較好一點


建立一個 Phoenix 專案 For GraphQL APIs


下集,我們要靠 Phoenix 這個 Web 專案當作基底,然後在上面蓋上 GraphQL , Phoenix 本身就是跑在 OTP Server 上,整個專案已經幫我們處理好基本架構的事務,只剩下我們需要對專案進行業務邏輯增減。


直接輸入指令:


mix phx.new drent --no-webpack --no-html

這會直接建立一個 drent 的專案。


專案說明:

我們要建立一個租借的系統,角色大致上有以下規劃:

  • Staff 員工
    • Profile 職員檔案
  • Rental 租借合約
    • 每個租借合約都有一個 Staff 員工
    • 租借合約有多個 Devices ,建立 Rental - Device Many-to-Many Table
  • Device 
    • 每個 Device 都有一個 Staff 作為擁有者


然後,我們要對這個專案加入 Dependencies,直接打開專案下的 mix.exs:,在 dependencies 處加上下面這三個套件:

defp deps do
    [
      {:absinthe, "~> 1.6"},
      {:absinthe_plug, "~> 1.5"},
      {:jason, "~> 1.1"},


這三個套件是針對 GraphQL 的支援,也就是 Absinthe 套件。

然後,輸入指令進行安裝:


mix deps.get


接著,你需要一個 PostgreSQL 資料庫,如果這個資料庫不是放在自己的電腦上,則需要到 config/dev.exs 檔案中去修改名稱。


然後,你要進去你的資料庫,去建立一個 DB:

CREATE DATABASE drent_dev;



建立 Business Model 與資料 Model


使用指令自動增加我們需要的 schema:


mix phx.gen.context Users Staff staffs fullname:string
mix phx.gen.context Users Profile profiles company:string, role:string
mix phx.gen.context Devices Device devices name:string sn:string
mix phx.gen.context Rentals Rental rentals title:string reason:string


以上四個 context generation,是針對業務需求所需要的 4 大類別,他會透過指令幫我們預先在 lib/drent/xxx 建好。


專案結構上,這篇文章現在的 Phoenix 是這麼分的:

  • lib/drent/   放 Repository Pattern 相關的類別,也就是這些單純的資料存取操作的工作以及 Typing
  • lib/drent_web 放所有與 Web 有關的東西,像是 GraphQL Resovler, REST API, WebSocket, ....etc,其中也包含 MVC 的 Views 和 Controllers

總之,新增這四個大類別之後,需要對他們的資料進行一些增修,總共要更動:

  • /lib/drent/devices/device.ex
  • /lib/drent/rentals/rental.ex
  • /lib/drent/users/staff.ex
  • /lib/drent/users/profile.ex


*注意 Phoenix Project 中所有的複數 s 詞,是會自動分出來而且是有意義在的,需要特別在有 s 和沒有 s 之間區分,尤其是 atom symbol 中,像是 :rental 和 :rentals, :staff 和 :staffs。


以下修改中,全部是在改關聯性,只有關聯性需要手動修正,裡面主要用到了 has_one, has_many, many_to_many, 以及屬於的 belongs_to 的操作。


belongs_to 跟 has_one 本質做的事情都一樣,只是所屬的角色不同,有一個人是 has_one,另一個人就要用 belongs_to ,如果不加 belongs_to, Ecto 恐怕不會幫你做很多關聯事務處理,事實上這不是在對資料庫做 Table 更動,Table 更動是等會要做的另一件事: Migration。 


/lib/drent/devices/device.ex:

schema "devices" do
    field :name, :string
    field :sn, :string

    # 一個裝置屬於一個擁有者
    belongs_to :staff, Drent.Users.Staff, foreign_key: :staff_id

    # 多個裝置可以有多個租借
    many_to_many :rentals, Drent.Rentals.Rental, join_through: "rentals_devices"

    timestamps()
end

/lib/drent/rentals/rental.ex:

schema "rentals" do
    field :reason, :string
    field :title, :string

    # 一個租借屬於一個 staff (要先有 staff 才有租借,所以不是用 has_one, 是 belongs_to)
    belongs_to :staff, Drent.Users.Staff

    # 不同的租借可以有不同很多個 devices
    many_to_many :devices, Drent.Devices.Device, join_through: "rentals_devices"


    timestamps()
end

/lib/drent/users/staff.ex:

schema "staffs" do
    field :fullname, :string

    # 一個 staff 有一個 profile
    has_one :profile, Drent.Users.Profile

    # 一個 staff 有很多的租借
    has_many :rentals, Drent.Rentals.Rental

    # 一個人可以是很多裝置的擁有者
    has_many :devices, Drent.Devices.Device
    timestamps()
end

/lib/drent/users/profile.ex:

schema "profiles" do
    field :company, :string
    field :role, :string

    # 一個 profile 屬於一個 staff
    belongs_to :staff, Drent.Users.Staff

    timestamps()
end

以上,完成對資料模型的更改,現在要做資料庫的關聯性,這要透過 Migrations 處理,原則上 Phoenix 在一開始你 gen 完這些 type models 的時候,都幫你在 priv/repo/migrations/* 建立好了,如果要自己手動建立,可以輸入指令:


mix ecto.gen.migration [your_custom_migration_name]


現在,我們要建立一個租借與裝置 (rentals_devices) 的 many-to-many 表,然後也對 Phoenix 先前建立好的 migration 一起做修改,請輸入指令:


mix ecto.gen.migration create_rentals_devices


然後要對以下檔案做變動:

  • /priv/repo/migrations/xxxxxxxx_create_devices.exs
  • /priv/repo/migrations/xxxxxxxx_create_rentals.exs
  • /priv/repo/migrations/xxxxxxxx_create_profiles.exs
  • /priv/repo/migrations/xxxxxxxx_create_rentals_devices.exs


/priv/repo/migrations/xxxxxxxx_create_devices.exs:

def change do
    create table(:devices) do
      add :name, :string
      add :sn, :string

      # 是嗎?
      # 一個裝置屬於一個擁有者
      add :staff_id, references(:staffs)

      timestamps()
    end

    # 注意不可這樣寫,這樣一個人只能擁有一個 devices
    # create unique_index(:devices, [:staff_id])
end

/priv/repo/migrations/xxxxxxxx_create_rentals.exs:

def change do
    create table(:rentals) do
      add :title, :string
      add :reason, :string

      # 一個租借屬於一個 staff
      add :staff_id, references(:staffs)
      timestamps()
    end

end

/priv/repo/migrations/xxxxxxxx_create_profiles.exs:

def change do
    create table(:profiles) do
      add :company, :string
      add :role, :string

      # profile 是屬於某一個 staff 的
      add :staff_id, references(:staffs)

      timestamps()
    end

end

以下這個是 many-to-many 的表,他沒有 type,所以要自己建:

/priv/repo/migrations/xxxxxxxx_create_rentals_devices.exs:

def change do
    create table(:rentals_devices) do
      add :rental_id, references(:rentals)
      add :device_id, references(:devices)
    end

    create unique_index(:rentals_devices, [:rental_id, :device_id])
end

以上都完成後,還剩下最後一個種子 Seeds 需要新增,這是給我們測試資料預設用的,我們甚至可以從這裡了解到一些資料結構方面的樣子,修改 priv/repo/seeds.exs:

alias Drent.Users.Staff
alias Drent.Users.Profile
alias Drent.Devices.Device
alias Drent.Rentals.Rental
alias Drent.Repo

# 先新增裝置
d1 = %Device{
  name: "Macbook Pro '15 2016",
  sn: "7712-1125-3262-2133"
} |> Repo.insert!
d2 = %Device{
  name: "Mac Pro Server AMD 2019",
  sn: "8584-1562-1656-1954"
} |> Repo.insert!
d3 = %Device{
  name: "Nighthawk® 12-Stream Dual-Band WiFi 6 Router (up to 6Gbps) with NETGEAR Armor™, MU-MIMO, USB 3.0 ports",
  sn: "5821-5262-7585-6325"
} |> Repo.insert!
d4 = %Device{
  name: "Surface Laptop i5 8G",
  sn: "1515-3262-8595-6216"
} |> Repo.insert!

# 新增 staffs
%Staff{
  fullname: "Fox",
  profile: %Profile{
    company: "ZFZ"
  },
  # 這個 staff 擁有哪些 devices
  devices: [
    d1,
    d2
  ],
  # 這個 staff 建立哪些租借合約? 租哪些裝置?
  rentals: [
    %Rental{
      reason: "會展需要租借電腦",
      title: "免費借用",
      devices: [
        d2, d3
      ]
    }
  ]
} |> Repo.insert!


%Staff{
  fullname: "Jamón",
  profile: %Profile{
    company: "OOP"
  },
  devices: [
   d3,
   d4
  ],
  rentals: [
    %Rental{
      reason: "會展需要租借電腦",
      title: "免費借用",
      devices: [
        d1, d3
      ]
    }
  ]
} |> Repo.insert!

現在,可以針對以上的 Migrations 和 Seeds 做完全設定,請直接跑指令:


mix ecto.setup


這個 setup 是針對資料庫一次性的,資料庫跑完一次,就會在資料庫的 migrations 表中記錄,因此如果要退回 setup,會需要 migration 寫 up, down ,或乾脆 drop database 然後重新來過。


要單獨跑 Seeds 也是可以的,指令是:


mix run priv/repo/seeds.exs


如果不想要包再一起寫 seeds.exs 像上面這種子結構 (%XXX{   devices: %VVV{ ... } }) 的形式的話,可以參考 build_assoc, put_assoc (接下來也會提及說明)


Ecto 查詢


一般的 Type Table 查詢,可以直接使用 Repo.get_by! 函數查詢, ! 是如果沒有資料,就會直接跳錯,如果不加驚嘆號,那你就必須自己使用 Pattern Matching 去自己客製化錯誤,等一下會做一個示範,以下是單純取得資料的 get_by,參數很簡單,只要放 Type 進去,後面接上 id 當作參數就好。


直接用指令開啟 iex -S mix 進入互動,然後進行上述的操作:


alias Drent.Repo
alias Drent.Users.Staff

Repo.get_by!(Staff, id: 1) #可以放任何 match 資料的參數

這裡所謂的 Type,是指在 /lib/drent/ (不是 web) 專案中定義的那些資料,像是上方的例子就是 /lib/drent/Users/Staff.ex 這個資料結構,上面這個查詢是查 Staff 為 1 的人。


然後,在此簡單探討驚嘆號的處理,在真正的程式存取操作中,不加驚嘆號的處理方法會變成:

case Repo.get_by(Staff, id: 1) do
      nil ->
        {:error, "NOT EXISTS!"}
      staff ->
        {:ok, staff}
end

這個 Pattern Matching 就可以幫你自動配對如果是 nil 的 sw case 或有一般變數 (此命名為: staff) 的 sw case,要做什麼事。


查詢方法


另一種查詢方式,也可以用 Build Query 的方式進行,他是這麼寫的:

# 需要引用 Ecto.Query 裡面的那些 macro
import Ecto.Query

#先定義 query
query = from(s in Staff, where: s.id == 1, select: s.fullname)

#然後拿去查所有配對到 id == 1 的資料
Repo.all(query)

也可以用另一種 Macro 的查詢方法:

# macro 的查詢方式:
query = select(Staff, [s], s.title)
Repo.all(query)


將變數放入查詢的特殊作法


Ecto 操作這些 query,必須要使用固定值,傳送固定值的方法是 pin 方法( ^ ) ,而不是使用變數,因為變數會變,可以想像成,把變數序列化 (Serialize) 這樣他就是定值,不會再變了。


# 在 where 中放變數來查詢

fullname = "Fox" # want to query fox matched

query = from(s in Staff, where: s.fullname == ^fullname, select: s.fullname)

Repo.all(query)

上面這個例子中,把 fullname 視為是傳進來的變數,而在配對的時候,使用了 ^fullname (pin 運算子) 來把值固定下來,這等同於 s.fullname == "Fox" ,Fox 就是 ^ pin 操作後的定值。 [A6]


關聯性查詢


現在,我們隨便查了一個 Repo.get(Staff, 1) ,你會得到 Staff 結構,可是你會發現有 profile 欄位、 devices 欄位都是 Not Loaded ,這些資訊不是從資料庫來的,是從 Elixir 你寫的定義中去定義關聯性的,所以它內建有功能可以幫你帶入這個關聯性。


一查完,得到這樣的資料:


%Drent.Users.Staff{
  __meta__: #Ecto.Schema.Metadata<:loaded, "staffs">,
  devices: #Ecto.Association.NotLoaded<association :devices is not loaded>,
  fullname: "Fox",
  id: 1,
  inserted_at: ~N[2021-05-23 09:22:06],
  profile: #Ecto.Association.NotLoaded<association :profile is not loaded>,
  rentals: #Ecto.Association.NotLoaded<association :rentals is not loaded>,
  updated_at: ~N[2021-05-23 09:22:06]
}

而載入的做法,其實只是要告訴 Ecto 你要載入誰而已,寫法比較 [A5]:


# 第一種方法
# 沒有 load 預載狀態
staff = Repo.get(Staff, 1) # 查 id 為 1 的資料

# 幫 Staff 上預載狀態 (預載 devices)
Ecto.assoc(staff, :devices)

# 幫 Staff 上預載狀態 (預載 profile)
Ecto.assoc(staff, :profile)

# 幫 Staff 上預載狀態 (預載 rentals)
Ecto.assoc(staff, :rentals)


# 第二種方法
staff = Repo.get(Staff, 1)
staff = Repo.preload(staff, :rentals)


# 第三種方法
# 也可以用這種方式預載,查出所有 :rentals 掛在這個 Staff 底下的人,也可以繼續加 ,:devices, :profile...
Repo.all(from s in Staff, preload: [:rentals])


如果你嘗試了這個例子,你會發現兩大問題需要解決: preload 的資料如果太多,要怎樣建分頁、每一個 Preload 做法,基本上直接查了兩次,要如何先進行優化,請參考下一節。



查詢優化



直接使用 preload 去做預載入,可以直觀的看到它一次查詢了兩個 query (看上面那個 Repo.all(...) 的例子),如果要對其進行優化,則可以通過以下手寫 join assoc 的方式減少查詢次數:

query = from(s in Staff, join: r in assoc(s, :rentals), preload: [rentals: r])

這個是用 join 的方式,一次 query 中就幫你用 join 查詢做到位。


Preload JOIN 操作



如果要在 join 的情況下做條件查詢,可以這麼做:

#欲查詢特定 rentals 的 id,可以直接用 where 比對查詢:
Repo.all(from(s in Staff, join: r in assoc(s, :rentals), where: r.id == 1 ,preload: [rentals: r]))



如果要查 LIKE 的方法,可以這麼做:

#欲查詢特定 rentals 的 title 狀況,可以使用 like 放在 where 裡面查詢試試看:

titlelike = "免費"
like_cond = "%#{titlelike}%"
query = from(s in Staff, join: r in assoc(s, :rentals), where: like(r.title, ^like_cond) ,preload: [rentals: r])


JOIN 操作


我們的結構中,Join 大部分都沒什麼問題,唯讀 Many-To-Many 的表要如何做 Join?,目前租借 Rentals 的 Devices 可以是 Many-To-Many,因此我們有這個表: rentals_devices,要做 Join 才會確切知道他們之間租借了什麼,不過因為 rentals_devices 不在定義的 Type 結構中,所以要用字串代替。


alias Drent.Rentals.Rental
alias Drent.Devices.Device

query = from r in Rental,
		join: mdtable in "rentals_devices",
		on: r.id == mdtable.rental_id,
		join: d in Device,
		on: d.id == mdtable.device_id,
		select: { r, d }

可以看到這麼做,就可以讓兩張表去 Join 了,而在 Join 表也可以做另外的條件查詢:

#我們也可以用 where 在 join 中查詢 d.id 為 1 的
query = from r in Rental,
		join: mdtable in "rentals_devices",
		on: r.id == mdtable.rental_id,
		join: d in Device,
		on: d.id == mdtable.device_id,
		where: d.id == 1,
		select: { r, d }


Sub-query with Join 的查法



關於分頁機制的問題,在這裡可以開始進行一個討論,如果要限制載入資料量,可以先設定限制,然後撈出資料來,而且限制資料的方式是用 Sub-query 來達成看看:

#也可以用 subquery 做到限制,雖然以下沒有認真細查 pagnition 情境,但用 limit 限制查詢到的資料有限,以節省效能

query_rental_id = 1

minimal_mdtable = from rd in "rentals_devices", where: rd.rental_id == ^query_rental_id, select: %{ rental_id: rd.rental_id, device_id: rd.device_id }, limit: 1

query = from r in Rental,
		join: mdtable in subquery(minimal_mdtable),
		on: r.id == mdtable.rental_id,
		join: d in Device,
		on: d.id == mdtable.device_id,
		where: r.id == ^query_rental_id,
		select: { r, d }

注意, minimal_mdtable 整個 type 就是 ecto 的 type,他放到 subquery 就會是另一個 table 加進來的形式了。


Fragment Raw 片段查法



fragment 可以幫助我們查詢一些 raw 相關的片段,也可以套用函數,和 ? 的 prepar statement 功能:

from s in Staff, 
	where: 
	fragment("staffs.id = ?", 1) == 1 
	and 
	fragment("lower(?)", "fox") == "fox"


業務邏輯需要的查詢


先寫一些之後要給 GraphQL 用的查詢:


列出使用者:


# 列出使用者

# GraphQL 也許可根據 __typename 去 preload 使用者的資料


# 新增使用者 + 新增 Profile 並同時綁定
alias Drent.Users.Profile
alias Drent.Users.Staff
alias Drent.Devices.Device
alias Drent.Rentals.Rental
alias Drent.Repo

# 之後要改寫 def 形式
new_user_with_profile = fn(fullname, company) -> 
	
	# 簡單新增結構
	staff = %Staff{
		fullname: fullname
	}
	
	# 新增到資料庫
	staff = Repo.insert!(staff)
	
	# 建立一個 profile 關聯性,而且是綁到 staff 上 (會自動綁到 staff_id 這個地方, staff_id 是自動的!!)
	profile = Ecto.build_assoc(staff, :profile, %{ company: company })
	
	# 建立 profile 並且含有關聯性
	Repo.insert!(profile)
end
# 呼叫方法: new_user_with_profile.("WAWA", "COOOOLMAN")

上述可以看到使用了 build_assoc 這個方法,這個方法是可以先把 staff 建出來,再把關聯性建上去的一種做法,可以適用 has_one, has_many,不適用 many-to-many。

新增 Devices 並設定 Devices 屬於某個使用者:


# 新增 Devices 並設定 Devices 屬於某個使用者

new_devices_belong_to_staff = fn(name, sn, staff_id) ->
	
	device = %Device{
		name: name,
		sn: sn,
		staff_id: staff_id # 原本我們做的 belongs_to 具有這個效應
	} |> Repo.insert!

	device
end
# 呼叫方法: new_devices_belong_to_staff.("NEW THINGS", "1515-2323-2323-6666", 1)

新增租借 + Devices 綁定到 rentals_devices 表:

這個部分就會需要使用 Ecto.Changeset.put_assoc/4 去做儲存,這是給 many_to_many 所使用的建立關聯性方法,build_assoc 是無法處理的,也因為 rentals devices 都沒有 foreign_keys,所以作為替代,要使用 Ecto 變更集跟 put_assoc 處理。

# 新增租借 + Devices 綁定到 rentals_devices 表
# 參數上, title: string, reason: string, staff_id: int
# 唯讀 [:devices | devs] 是辨別陣列的 Pattern Matching:
# 用法上會像是: [:devices, 1, 2, ,3, 4],一開頭先告訴這個 pattern 有 :devices 這個 symbol 作為起始
# 這樣 devs 就會是 list [1,2,3,4]
new_rental_bind_devices = fn (title, reason, staff_id, [:devices | devs]) -> 
	
	# 新增一個租借,並新增到資料庫
	rent = %Rental {
		title: title,
		reason: reason,
		staff_id: staff_id,
	} 
	|> Repo.insert!
	
	# 這是很特別的做法,因為 [:devices | devs ] 傳進來都是 int 陣列, 我們假裝他是預載過的資料
	# 因為實際上建立關聯性不需要那麼多資訊
	# for 會自動建起 list, 每個元素都是 do 裡面產生的
	devices = for d <- devs do
		%Device{ id: d, __meta__: %Ecto.Schema.Metadata{ source: :devices, state: :loaded } }
	end
	
	# 預載資料 (尤其 :devices 需要關聯性)、建立變更集、將關聯性資料放到 devices 中、更新
	rent
	|> Repo.preload([:devices, :staff])
	|> Ecto.Changeset.change()
	|> Ecto.Changeset.put_assoc(:devices, devices)
	|> Repo.update()

end
# 呼叫方法: new_rental_bind_devices.("FREE RENT", "I NEED IT", 1, [:devices, 1, 2, 3])


上述有提到一個很特別的作法: devices = [假預載資料] ,社群中的朋友們認為, devices 應該都要做 Repo.get_by!() 去載入每一個 device 結構,那樣結構就不再是 [ :devices, 1,2,3,4] 而會變成 [ :devices, %Device{}, %Device{}...],而上述這個寫法,也是社群一部分朋友認為建立關聯性只是要用到 id ,需要兩邊的關聯性資料全部都要帶出來才能建立嗎? [A7] [A8] [A9] [A10]



變更集 Changeset, Insert, Update, Remove


Insert, Remove 似乎都是 Repo 內建就幫我們辦到的事情,唯有 Update 還需要再思考一下,變更集是 Ecto 的一個解決方案,他可以用模組的形式做資料更改,也可以做到確認資料的驗證、淺在錯誤、正確性。


詳情可以參考: https://elixirschool.com/zh-hant/lessons/ecto/changesets/


在這裡使用 Changeset 示範如何變更 Staff 姓名 [A11]:


# 修改某個使用者的 fullname

change_staffname_by_id = fn(staff_id, fullname) ->
	alias Drent.Users.Staff
	alias Drent.Repo

	Repo.update(Ecto.Changeset.cast(%Staff{ id: (staff_id,  }, %{ "fullname" => fullname)  }, [:fullname]))
end
# 呼叫方法: change_staffname_by_id .(1, "FOX 2")


你可能會需要帶出資料,或是像上面使用 %Staff{} 具體有 id 結構的東西,然後做成 changeset,沒辦法單純做更新。 詳情可以參考 [A12] - Chapter 8. Making changes with Ecto.Changeset - 8.1. Can’t I just ... update? : Nope. You can’t.



2021/02/20 著作, 2021/05/24 發佈。


References:

http://blog.plataformatec.com.br/2015/08/working-with-ecto-associations-and-embeds/
https://elixirforum.com/t/how-to-seed-pivot-table-relations/4494/4
https://hexdocs.pm/ecto/Ecto.html#build_assoc/3
https://elixirschool.com/zh-hant/lessons/ecto/associations/
https://hexdocs.pm/ecto/Ecto.Query.html
https://elixirforum.com/t/ecto-not-allowing-string-interpolation-in-fragments/28459/3
https://elixirforum.com/t/seeding-database-with-relationships/13240
https://hexdocs.pm/ecto/Ecto.html#build_assoc/3
https://elixirforum.com/t/help-filtering-many-to-many-associations-with-ecto/4210/4
https://riptutorial.com/elixir/example/6956/pattern-matching-on-a-list
https://hexdocs.pm/ecto/Ecto.Schema.html#has_many/3
https://elixirforum.com/t/build-assoc-vs-put-assoc/24071
https://medium.com/@andreichernykh/thoughts-on-structuring-an-elixir-phoenix-project-cb083a8894ef
[A5] https://stackoverflow.com/questions/39896713/how-can-i-preload-association-and-get-it-returned-in-ecto
[A6] https://stackoverflow.com/questions/33324302/what-are-elixir-bang-functions
[A7] https://hexdocs.pm/ecto/Ecto.Changeset.html#put_assoc/4
[A8] https://gist.github.com/cblavier/356e662fdbc7910cc4bc39737d7851c2
[A9] https://elixirforum.com/t/updating-many-to-many-with-put-assoc-and-an-array-of-ids/8648/2
[A10] https://elixirschool.com/zh-hant/lessons/ecto/associations/#%E5%84%B2%E5%AD%98%E9%97%9C%E8%81%AF%E8%B3%87%E6%96%99
[A11] https://hexdocs.pm/ecto/Ecto.Changeset.html#cast/4
[A12] https://livebook.manning.com/book/phoenix-in-action/chapter-8/9


沒有留言:

張貼留言

© Mac Taylor, 歡迎自由轉貼。
Background Email Pattern by Toby Elliott
Since 2014