Phoenix-Framework: преобразование, сопоставление и нулевая проверка родительской модели Ecto при создании дочерней модели

Я перехожу на фреймворк Phoenix из Rails. До сих пор это был довольно легкий переход. Однако Phoenix новее, и у меня возникли проблемы с поиском конкретной информации:

Я использую приложение Phoenix в качестве сервисного слоя API.
Я хочу, чтобы моя форма пользовательского интерфейса (и входящие запросы curl) использовала virtual field для поиска связанной родительской модели и заполнила changeset дочерней модели соответствующим атрибутом. Все идет нормально:

в моей дочерней модели:

schema "child" do
    field :parent_name, :string, virtual: true
    belongs_to :parent, MyApp.Parent
end

...

before_insert :find_and_fill_parent

    def find_and_fill_parent(changeset) do
        parent = MyApp.Repo.get_by(
                  MyApp.Parent, 
                  parent_name: changeset.changes.parent_name
                 )

        changeset
        |> Ecto.Changeset.put_change(:parent_id, parent.id)
    end

Вот где я застрял. Я не хочу, чтобы ребенок был создан без родителя. Как и где nil проверить родительскую модель? Все, что я пробовал, либо блокировало, либо разрешало создание безоговорочно, несмотря на условные операторы.

Похоже, мне нужно предварительно загрузить родительскую модель перед проверкой, поскольку Phoenix разработан, чтобы предотвратить злоупотребление отложенной загрузкой такими людьми, как я. . К сожалению, я не знаю, какова в целом правильная схема нагрузки, поэтому я не уверен, как ее применить здесь. (Я использую MySQL, если это уместно)

Советы и подсказки о том, где искать и на что смотреть, чтобы помочь мне понять это, приветствуются! Спасибо! ????

---EDIT---
По совету @Gazler я убедился, что у миграции моей дочерней модели есть ссылка:

create table(:child) do
    add :parent_id, references(:parent)
end

Я все еще немного потерян - я хочу найти parent по родительскому полю parent_name ("Jane Doe"), убедиться, что родительская модель существует, и связать дочерний элемент с помощью parent_id (1). Я не уверен, как инициировать эти действия с использованием виртуального поля.

Итак, я не уверен, как структурировать поиск родителя, создание ассоциации и проверку проверки внешнего ключа, поскольку внешний ключ никогда не будет существовать в исходных параметрах. Мысли?

Огромное спасибо.

---РЕШЕНО---
Используя обновленный ответ @Gazler, я могу успешно проверить мою родительскую модель в дочернем контроллере без виртуального атрибута или before_insert.

def create(conn, %{"post" => post_params}) do 
    user = Repo.get_by(User, %{name: post_params["name"]})
    if is_nil(user) do
        changeset = Post.changeset(%Post{}, post_params)
    else
        changeset = build(user, :posts) |> Post.changeset(post_params)
    end
    etc
end

это проверяет входящие параметры именно так, как мне нужно! спасибо @Газлер!


person foolsgoldfish    schedule 07.10.2015    source источник


Ответы (1)


before_insert, вероятно, не самое подходящее место для этого.

Вы используете ассоциацию belongs_to, однако, если вы включите ассоциацию has_many на другой стороне, вы можете использовать build/3 для заполнения родительского идентификатора:

build(user, :posts)

Это просто функция, которая заполняет поле post_id для структуры — она не будет проверять, действительно ли существует user.

Если вы хотите убедиться, что пользователь существует до создания сообщения, вы можете использовать foreign_key_constraint/ 3 в вашем наборе изменений:

cast(comment, params, ~w(user_id), ~w())
|> foreign_key_constraint(:user_id)

Это гарантирует на уровне базы данных, что запись для родителя существует. Вам нужно иметь индекс в вашей базе данных, который будет создан, если ваша миграция будет выглядеть так:

create table(:posts) do
  add :user_id, references(:user)
end

ИЗМЕНИТЬ

Нет необходимости в виртуальном атрибуте. Действие вашего контроллера должно выглядеть примерно так:

def create(conn, %{"name" => name, "post" => post_params}) do
  user = Repo.get_by(User, %{name: name})
  changeset = build(user, :posts) |> Post.changeset(post_params)
  case Repo.insert(changeset) do
    {:ok, post} -> ...
    {:error, changeset} -> #if constraint is violated then the error will be in the changeset
  end
end
person Gazler    schedule 07.10.2015
comment
ах - моя миграция не включала references(:user), я думаю, что я более или менее добавлял ассоциацию задним числом на стороне кода. есть ли способ написать миграцию изменений, чтобы добавить эту ассоциацию? не показалось... - person foolsgoldfish; 07.10.2015
comment
Большое спасибо за ваш ответ, однако я бы не подумал об этом так, как вы предлагаете; я попробую. если нужно, я могу переписать базу данных, я еще не очень далеко продвинулся (очевидно ????) - person foolsgoldfish; 07.10.2015
comment
Ознакомьтесь с hexdocs.pm/ecto/Ecto.Migration.html#alter/2 для изменения таблицы. Если вы еще не запустили миграцию, вы можете просто изменить оригинал и выполнить откат/миграцию. - person Gazler; 07.10.2015
comment
спасибо за ваш быстрый ответ, я ценю это! я пытался просмотреть ваши предложения, и у меня все еще не получается сопоставить их с тем, что я делаю. я обновил исходный вопрос - person foolsgoldfish; 07.10.2015
comment
Я внес изменения в ответ, надеюсь, это прояснит ситуацию. - person Gazler; 07.10.2015
comment
я понимаю теперь, и это работает отлично. большое спасибо за ответ на мой вопрос! - person foolsgoldfish; 07.10.2015
comment
Рад, что смог помочь. Если вы обнаружите, что часто проверяете присутствие пользователя, вам следует изучить возможность использования плагина для получения вашего пользователя перед запросом. - person Gazler; 07.10.2015
comment
отличное и своевременное предложение - обязательно сделаю :) - person foolsgoldfish; 07.10.2015