【问题标题】:How to represent this query in Elixir's Ecto Query format?如何用 Elixir 的 Ecto Query 格式表示这个查询?
【发布时间】:2017-10-09 17:26:12
【问题描述】:

我无法将以下 SQL 查询“翻译”为有效的 Ecto 查询:

SELECT *
FROM file_modules 
WHERE file_id =
  (SELECT f.file_id
  FROM files AS f
  LEFT JOIN file_modules AS fm ON f.file_id = fm.file_id
  WHERE f.storage_id = '20:1:0:86d:1591:c89c:512:de52' AND fm.name = 'bruteforce'
  ORDER BY f.version DESC
  LIMIT 1)

我相信要走的路是将这个查询分成两部分。这是我尝试过的:

q1 = 
  File
  |> where([f], f.storage_id == ^storage_id)
  |> join(:left, [f], fm in FileModule, f.file_id == fm.file_id)
  |> where([..., fm], fm.name == ^module_name)
  |> order_by([..., fm], desc: fm.version)
  |> select([fm], fm.file_id)
  |> limit(1)

# q1 works and returns the expected file_id

q2 =
  q1
  |> subquery()
  |> where([fm, fm2], fm.file_id == fm2.file_id)  # Here's where I'm stuck
  |> preload([..., m], [modules: m])

q1 的 Ecto.Query 结果如下:

#Ecto.Query<from f0 in Helix.Software.Model.File,
 left_join: f1 in Helix.Software.Model.FileModule, on: f0.file_id == f1.file_id,
 where: f0.storage_id == ^(storage_id),
 where: f1.name == ^:bruteforce, order_by: [desc: f1.version], limit: 1,
 select: f0.file_id>

我的问题似乎与q2 有关。我应该如何格式化它才能使用 结果来自q1 作为输入?

提前致谢。


我相信这个问题与我的基础架构无关,但它是:

schema "files" do
  field :file_id, ID,
    primary_key: true

  field :name, :string
  field :path, :string
  field :software_type, Constant
  field :file_size, :integer
  field :storage_id, Storage.ID

  field :crypto_version, :integer

  field :full_path, :string

  belongs_to :type, SoftwareType,
    foreign_key: :software_type,
    references: :software_type,
    define_field: false
  belongs_to :storage, Storage,
    foreign_key: :storage_id,
    references: :storage_id,
    define_field: false

  has_many :modules, FileModule,
    foreign_key: :file_id,
    references: :file_id,
    on_replace: :delete

  timestamps()
end



schema "file_modules" do
  field :file_id, File.ID,
    primary_key: true
  field :name, Constant,
    primary_key: true
  field :version, :integer

  belongs_to :file, File,
    foreign_key: :file_id,
    references: :file_id,
    define_field: false,
    on_replace: :update
end

正如 Jabnović 在 cmets 上提到的,ORM 抽象有时可能会失败。在那种情况下,是否可以使用上面的原始 sql 而:

1) 强制转换 File.ID 和 Storage.ID 类型,以避免丑陋的字符串连接?

2) 返回结果后,是否将此结果预加载或保存到架构中?

我想到的界面是这样的:

q = "SELECT * FROM files WHERE file_id = $1", ^file_id
file = Repo.get(q) |> Repo.preload()

file.file_id  # Returns file id

按照Ecto.Adapters.SQL.query/4的例子,看来我可以做到1。2呢?

我的最终目标是使用上面的嵌套 SQL 查询,同时安全地转换 file_id 和 storage_id,并正确使用 %File{} 架构,并在 file.modules 加载关联。

请注意,存储过程或视图会给我一个更好的界面,我可以使用 fragment 正确转换,但我觉得我仍然有将数据“预加载”到架构中的问题。

【问题讨论】:

  • 为什么不分别执行这两个查询呢?你不会损失太多。
  • 这就是我现在正在做的事情,作为一种解决方法.. 但由于无法使用子查询,我最终会受到极大的限制。我确信 Ecto 支持,所以我要弄清楚它是如何工作的。但到目前为止,即使专门阅读了几篇关于 Ecto 子查询的文章和指南,我仍然无法理解它。
  • 不幸的是,在过去使用多个 ORM 之后,我可以说每个人迟早都会hits the wall 尝试将 SQL 查询转换为 ORM 表达式。有时,将 SQL 保持原样,无论是在字符串中,还是在某种形式的数据库视图中,都可以为您和未来的维护人员节省大量时间。
  • 说实话,我在同一个阵营,我更喜欢 SQL 而不是 ORM。在这种情况下,是否有任何合理的方法可以混合“原始”sql 查询,同时从 Ecto 的转换中获得一些类型安全,并且能够将返回的数据预加载到模式中? (我已经编辑了底部的问题)

标签: elixir ecto


【解决方案1】:

Subquerys are currently not allowed in where clauses; the documentation recommends using a JOIN instead.

我还没有测试过,但是下面的查询应该会产生与你原来的 SQL 相同的结果:

q1 = ... # Same as in the question.
q2 =
  FileModule
  |> join(:left, [fm], fid in subquery(q1), fm.file_id == fid)
  |> select([fm], fm)

【讨论】:

    【解决方案2】:

    我不打算回答我自己的问题,但嘿,这是我想出的另一种解决方案。如问题所述,如果我能找到一个不错的界面让我:

    • 使用原始 SQL 查询
    • 同时仍然利用 Ecto 类型转换
    • 并且能够将关联数据预加载到架构中。

    通过混合使用Repo.loadEcto.Adapters.SQL.query,我能够做到这一点。如果对其他人有帮助,我会分享接口和代码。

    sql = "
      SELECT *
      FROM file_modules 
      WHERE file_id =
        (SELECT f.file_id
        FROM files AS f
        LEFT JOIN file_modules AS fm ON f.file_id = fm.file_id
        WHERE f.storage_id = ##1::storage_id AND fm.name = ##2::module_name
        ORDER BY f.version DESC
        LIMIT 1)"
    
    caster = fn type, value -> 
      case type do
        :storage_id ->
          Storage.ID.cast!(value) && to_string(value)
        :module_name ->
          Software.Module.exists?(value) && value || {:error, :bad_value}
        _ ->
          Hector.std_caster(type, value)
      end
    end
    
    query = Hector.query(sql, [storage_id, module_name], caster)
    
    loader = fn repo, {columns, rows} ->
      rows
      |> Enum.map(fn row ->
        repo
        |> apply(:load, [File, {columns, row}])
        |> File.format()
      end)
    end
    
    {:ok, entries} = Hector.get(Repo, query, loader)
    
    # Where `entries` is a list of %File{}, a valid Ecto Schema.
    

    Hector 是处理这个接口的库。上面的例子是最复杂的情​​况:我们需要一个自定义的 caster 和一个自定义的加载器。在大多数情况下,Hector 的默认加载器就可以正常工作,并且只要存在潜在的不安全输入,就需要自定义施法器。

    可以在here 找到代码,tests 上有一些额外的示例。

    当然,这远非最佳解决方案,尽管我可能能够为这个或那个查询找出正确的 Ecto 语法,但为原始查询提供一个好的抽象/接口总是很方便的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-10-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多