我最近开始将现有的PHP Messenger应用程序移植到elixir(使用elixir 1.3,phoenix 1.2,ecto 2.0.1和mariaex 0.7.7)。这个应用程序服务数百万用户,因此性能很重要。我对Elixir很新,所以请原谅我问题的愚蠢
我有以下数据库架构:
每个线程都有多个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'
}
}
以下是Thread和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
我尝试将此转换为Ecto(甚至使用子查询或片段)都失败了(子查询中没有Max(),子查询中没有字段别名,...)
因此,除了第一个查询(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放在参与者键下第一个查询的每个Thread中),然后我如何输出它,规范化,在我看来(只有参与者ID被保存在thread.participants下,所有不同的用户都在用户下输出)
我坚持了几个小时,我真的很感激你能分享的任何知识
答案 0 :(得分:0)
我最终得到了一切。经过几个小时重新发明轮子(即在第二个查询中加载thread_participants然后通过线程列表添加他们的参与者),我注意到无论你在第一个查询中放置什么,ecto将在单独的查询中获取预加载的关联
因此,为了解决问题1(如何合并两个结果集),解决方案是:不要这样做:-)只需将所需的关联标记为预加载。只要您在主查询中加载了线程ID,ecto就会乐意为您完成艰苦的工作:
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
在调试模式下,您可以看到ecto执行以下查询:
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的地图,列表和枚举,就会非常简单:
控制器将线程列表传递给具有以下代码的视图:
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这种美妙的语言蘸着你的脚趾。