2016-06-30 68 views
1

我最近開始將現有的PHP messenger應用程序移植到elixir(使用elixir 1.3,phoenix 1.2,ecto 2.0.1和mariaex 0.7.7)。這個應用程序爲數百萬用戶提供服務,因此性能很重要我很新的藥劑所以請原諒我的問題Elixir&ecto:子查詢或側加載和規範化json輸出的視圖

的啞我有以下的數據庫模式:

Database schema

每個線程都有多個thread_participants和消息。 thread_participant在鏈接線程的上下文中有關於用戶的信息(例如,當用戶上次看到此線程時)。線程由用戶創作的多條消息組成。

我想什麼JSON我的API到底返回是:

"data": { 
    "result": [1, 2], 
    "threads": { 
    1: { 
     "id": 1, 
     "unread_count": 2, 
     "starred": false, 
     "muted": false, 
     "last_seen": "2015-10-20T19:01:46", 
     "participants": [1, 2] 
    }, 
    22: { 
     "id": 22, 
     "unread_count": 0, 
     "starred": true, 
     "muted": false, 
     "last_seen": "2016-06-20T12:00:00", 
     "participants": [1, 3] 
    } 
    }, 
    users: { 
    1: { 
     id: 1, 
     name: 'John' 
    }, 
    2: { 
     id: 2, 
     name: 'Dan' 
    }, 
    3: { 
     id: 3, 
     name: 'Eric' 
    }   
    } 

這裏是我的主題和ThreadParticipant模式:

schema "thread" do 
    field :created, Ecto.DateTime, usec: true, autogenerate: true 
    belongs_to :creator, UserAbstract 
    has_many :messages, ThreadMessage 
    has_many :participants, ThreadParticipant 
    has_many :users, through: [:participants, :user] 
    field :last_seen, Ecto.DateTime, virtual: true, default: :null 
    field :muted, :boolean, virtual: true, default: false 
    field :starred, :boolean, virtual: true, default: false 
    field :unread_count, :integer, virtual: true, default: 0 
end 

@primary_key false 
schema "thread_participant" do 
    belongs_to :thread, Messenger.Thread, primary_key: true 
    belongs_to :user, Messenger.UserAbstract, primary_key: true 
    field :last_seen, Ecto.DateTime, usec: true, autogenerate: true 
    field :starred, :boolean, default: false 
    field :muted, :boolean, default: false 
end 

,我使用查詢組成的背景情況線程列表爲用戶:

def for_user(query, user_id) do 
    from t in query, 
    join: p in assoc(t, :participants), 
    join: message in assoc(t, :messages), 
    left_join: messageNew in ThreadMessage, on: messageNew.id == message.id and messageNew.created > p.last_seen, 
    where: p.user_id == ^user_id, 
    order_by: [desc: max(message.created)], 
    group_by: t.id, 
    select: %{ t | last_seen: p.last_seen, muted: p.muted,starred: p.starred, unread_count: count(messageNew.id)} 
end 

所以當我做

Thread |> Thread.for_user(user_id) |> Repo.all 

我能夠獲得幾乎所有正確的聚集信息,但由於group_by thread.id,我錯過了參與者ID。

在純SQL我會做類似下面的代碼,然後重建我的模型代碼:

SELECT s.id, s.last_seen, s.muted, s.starred, s.last_message_date, s.unread_count, p.user_id 
    FROM (
    SELECT t0.`id` , t2.`last_seen` , t2.`muted` , t2.`starred` , max(t1.`created`) as last_message_date, count(t3.id) as unread_count 
    FROM `thread` AS t0 
    INNER JOIN `thread_message` AS t1 ON t0.`id` = t1.`thread_id` 
    INNER JOIN `thread_participant` AS t2 ON (t0.`id` = t2.`thread_id`) AND (t2.`user_id` = 9854) 
    LEFT JOIN `thread_message` AS t3 ON t3.`id` = t1.`id` AND t3.`created` > t2.`last_seen` 
    GROUP BY t0.`id` 
) as s 
    INNER JOIN `thread_participant` AS p ON p.`thread_id` = s.`id` 
    ORDER BY s.`last_message_date` DESC 

我所有的嘗試翻譯這外生(即使使用子查詢或片斷)都失敗了(沒有最大()在子查詢,子查詢字段別名不被保存,...)

所以除了第一查詢(for_user()),我加載第二個查詢參與者:

thread_ids = Enum.map(threads, fn (x) -> x.id end) 

def get_participating_user(thread_ids) do 
    from tp in ThreadParticipant, 
    join: user in assoc(tp, :user), 
    where: tp.thread_id in ^thread_ids, 
    preload: :user 
end 

participants = Thread.get_participating_user(thread_ids) |> Repo.all 

但現在我被困在如何合併兩個結果集(把ThreadParticipants從第二個查詢中他們屬於每個線程中的參與者鍵下的第一個查詢中),然後如何輸出它,規範化,在我看來(只參加者的ID都保持在thread.participants和所有不同的用戶可根據用戶輸出)

已經停留在此幾個小時,我會很感激的任何知識,你可以分享

回答

0

我最終讓一切工作。經過許多小時後重新發明輪子(即在第二個查詢中加載thread_participants,然後遍歷線程列表以添加其參與者),我注意到,無論您放入第一個查詢中,ecto都會在單獨的查詢中獲取預加載的關聯。

所以要解決問題1(我如何合併兩個結果集),解決方案是:不要這樣做:-)只需將所需的關聯標記爲預加載。只要你有你的主查詢外生加載的線程ID會很樂意做的辛勤工作爲您提供:

def for_user(query, user_id) do 
    from t in query, 
    join: p in assoc(t, :participants), 
    join: message in assoc(t, :messages), 
    join: u in assoc(p, :user), 
    left_join: messageNew in ThreadMessage, on: messageNew.id == message.id and messageNew.created > p.last_seen, 
    where: p.user_id == ^user_id, 
    order_by: [desc: max(message.created)], 
    group_by: t.id, 
    preload: [:participants,:users], 
    select: %{ t | last_seen: p.last_seen, muted: p.muted,starred: p.starred, unread_count: count(messageNew.id)} 
    end 

在調試模式下,你可以看到,外生做以下查詢:

SELECT t0.`id`, t0.`created`, t0.`creator_id`, t1.`last_seen`, t1.`muted`, t1.`starred`, count(t4.`id`) FROM `thread` AS t0 INNER JOIN `thread_participant` AS t1 ON t1.`thread_id` = t0.`id` INNER JOIN `thread_message` AS t2 ON t2.`thread_id` = t0.`id` INNER JOIN `user` AS u3 ON u3.`id` = t1.`user_id` LEFT OUTER JOIN `thread_message` AS t4 ON (t4.`id` = t2.`id`) AND (t4.`created` > t1.`last_seen`) WHERE (t1.`user_id` = ?) GROUP BY t0.`id` ORDER BY max(t2.`created`) DESC LIMIT 5 [20] 

SELECT t0.`thread_id`, t0.`user_id`, t0.`last_seen`, t0.`starred`, t0.`muted`, t0.`thread_id` FROM `thread_participant` AS t0 WHERE (t0.`thread_id` IN (?,?,?,?,?)) ORDER BY t0.`thread_id` [45, 47, 66, 77, 88] 

SELECT u0.`id`, u0.`display_name`, u0.`id` FROM `user` AS u0 WHERE (u0.`id` IN (?,?,?,?,?,?)) [10, 11, 12, 13, 14, 15] 

解決問題2(在我看來,如何輸出它,規範化(只有參與者ID保留在thread.participants下,並且所有不同的用戶都在用戶的輸出下)),這很簡單,一旦你開始理解elixir的地圖,列表和枚舉:

控制器R中的線程列表傳遞給它具有以下代碼視圖:

def render("index.json", %{thread: threads}) do 
%{ data: 
    %{ 
    threads: render_many(threads, Messenger.ThreadView, "user_thread.json"), 
    users: render_many(threads |> Stream.flat_map(&(&1.users)) |> Stream.uniq, Messenger.UserAbstractView, "user_abstract.json") 
    } 
} 

def render("user_thread.json", %{thread: thread}) do 
    %{id: thread.id, 
    last_seen: thread.last_seen, 
    muted: thread.muted, 
    starred: thread.starred, 
    unread_count: thread.unread_count, 
    participants: Enum.map(thread.participants, fn(tp) -> tp.user_id end) 
    } 
end 

棘手的部分:

#Here we extract a list of uniq users from our list of threads 
#and use our user view to render them 
users: render_many(threads |> Stream.flat_map(&(&1.users)) |> Stream.uniq, Messenger.UserAbstractView, "user_abstract.json") 

#Here we populate the participants key with a list of the participants ids 
participants: Enum.map(thread.participants, fn(tp) -> tp.user_id end) 

而且你去那裏! - >規範化的結構。

希望它能爲您節省一些時間,如果像我一樣,用Elixir這種美妙的語言浸泡腳趾。