object: add support for preloading objects when walking an activity graph in normal...
authorWilliam Pitcock <nenolod@dereferenced.org>
Fri, 22 Mar 2019 23:34:47 +0000 (23:34 +0000)
committerWilliam Pitcock <nenolod@dereferenced.org>
Fri, 22 Mar 2019 23:51:12 +0000 (23:51 +0000)
lib/pleroma/activity.ex
lib/pleroma/object.ex

index 04c3bb6737a6dbc5d469cc1b9a8ef52f729cca99..2f91b3c7710af16536dd7080c86597a75ccb0fc3 100644 (file)
@@ -7,6 +7,7 @@ defmodule Pleroma.Activity do
 
   alias Pleroma.Activity
   alias Pleroma.Notification
+  alias Pleroma.Object
   alias Pleroma.Repo
 
   import Ecto.Query
@@ -33,6 +34,18 @@ defmodule Pleroma.Activity do
     field(:recipients, {:array, :string})
     has_many(:notifications, Notification, on_delete: :delete_all)
 
+    # Attention: this is a fake relation, don't try to preload it blindly and expect it to work!
+    # The foreign key is embedded in a jsonb field.
+    #
+    # To use it, you probably want to do an inner join and a preload:
+    #
+    # ```
+    # |> join(:inner, [activity], o in Object,
+    #    fragment("(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)", o.data, activity.data))
+    # |> preload([activity, object], [object: object])
+    # ```
+    has_one(:object, Object, on_delete: :nothing, foreign_key: :id)
+
     timestamps()
   end
 
@@ -49,6 +62,21 @@ defmodule Pleroma.Activity do
     Repo.get(Activity, id)
   end
 
+  def get_by_id_with_object(id) do
+    from(activity in Activity,
+      where: activity.id == ^id,
+      inner_join: o in Object,
+      on:
+        fragment(
+          "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
+          o.data,
+          activity.data
+        ),
+      preload: [object: o]
+    )
+    |> Repo.one()
+  end
+
   def by_object_ap_id(ap_id) do
     from(
       activity in Activity,
@@ -76,7 +104,7 @@ defmodule Pleroma.Activity do
     )
   end
 
-  def create_by_object_ap_id(ap_id) do
+  def create_by_object_ap_id(ap_id) when is_binary(ap_id) do
     from(
       activity in Activity,
       where:
@@ -90,6 +118,8 @@ defmodule Pleroma.Activity do
     )
   end
 
+  def create_by_object_ap_id(_), do: nil
+
   def get_all_create_by_object_ap_id(ap_id) do
     Repo.all(create_by_object_ap_id(ap_id))
   end
@@ -101,6 +131,36 @@ defmodule Pleroma.Activity do
 
   def get_create_by_object_ap_id(_), do: nil
 
+  def create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
+    from(
+      activity in Activity,
+      where:
+        fragment(
+          "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
+          activity.data,
+          activity.data,
+          ^to_string(ap_id)
+        ),
+      where: fragment("(?)->>'type' = 'Create'", activity.data),
+      inner_join: o in Object,
+      on:
+        fragment(
+          "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
+          o.data,
+          activity.data
+        ),
+      preload: [object: o]
+    )
+  end
+
+  def create_by_object_ap_id_with_object(_), do: nil
+
+  def get_create_by_object_ap_id_with_object(ap_id) do
+    ap_id
+    |> create_by_object_ap_id_with_object()
+    |> Repo.one()
+  end
+
   def normalize(obj) when is_map(obj), do: Activity.get_by_ap_id(obj["id"])
   def normalize(ap_id) when is_binary(ap_id), do: Activity.get_by_ap_id(ap_id)
   def normalize(_), do: nil
index 58e46ef1d4dcb1771ca1b377f1360abfae7bf2d4..0f5c532eceedec1a32599503bb958914ffe30d41 100644 (file)
@@ -14,6 +14,8 @@ defmodule Pleroma.Object do
   import Ecto.Query
   import Ecto.Changeset
 
+  require Logger
+
   schema "objects" do
     field(:data, :map)
 
@@ -38,6 +40,29 @@ defmodule Pleroma.Object do
     Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
   end
 
+  # If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
+  # Use this whenever possible, especially when walking graphs in an O(N) loop!
+  def normalize(%Activity{object: %Object{} = object}), do: object
+
+  # Catch and log Object.normalize() calls where the Activity's child object is not
+  # preloaded.
+  def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}) do
+    Logger.info(
+      "Object.normalize() called without preloaded object (#{ap_id}).  Consider preloading the object!"
+    )
+
+    normalize(ap_id)
+  end
+
+  def normalize(%Activity{data: %{"object" => ap_id}}) do
+    Logger.info(
+      "Object.normalize() called without preloaded object (#{ap_id}).  Consider preloading the object!"
+    )
+
+    normalize(ap_id)
+  end
+
+  # Old way, try fetching the object through cache.
   def normalize(%{"id" => ap_id}), do: normalize(ap_id)
   def normalize(ap_id) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)
   def normalize(_), do: nil