Elixir& ecto:查看

时间:2016-06-30 21:49:21

标签: elixir phoenix-framework ecto

我最近开始将现有的PHP Messenger应用程序移植到elixir(使用elixir 1.3,phoenix 1.2,ecto 2.0.1和mariaex 0.7.7)。这个应用程序服务数百万用户,因此性能很重要。我对Elixir很新,所以请原谅我问题的愚蠢

我有以下数据库架构:

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'
    }        
  }

以下是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下,所有不同的用户都在用户下输出)

我坚持了几个小时,我真的很感激你能分享的任何知识

1 个答案:

答案 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这种美妙的语言蘸着你的脚趾。