Laravel Eloquent: есть ли способ обновить БД с помощью динамических полей?

Я пытаюсь реализовать функцию «Изменить настройки приложения». Немного подумав, мои значения конфигурации хранятся в БД со структурой ключ -> значение, например:

идентификатор ключ ценить 1 путь_логотипа изображение/логотип.png

Как видите, для каждого параметра есть только столбец «ключ и значение». Я создал поставщика службы приложений для их кэширования навсегда и вспомогательную функцию (config('setting_key')) для получения значения, но теперь я хотел бы обновить его наиболее эффективным способом.

Пользовательский интерфейс состоит из <form action = "post" ...> и ввода с соответствующим именем, например: <input name = "setting_key_name" ... />. Как видите, атрибут name здесь имеет значение столбца key, а фактическое значение ввода будет значением столбца value (здесь немного путаницы).

Первое, что пришло мне в голову, это сделать цикл foreach и найти и обновить каждую строку в БД, но ИМХО это очень неоптимизированный способ, потому что если на странице есть форма с 10 значениями, это 10 запросов SQL. Но до сих пор вот что я сделал:

$keys = collect($request->except('_token'))->keys()->toArray();

// get all settings if the key name matches the request's input name
$setting = Setting::whereIn('key', $keys)->get();

$logo = self::GENERAL_APP_LOGO; // contant with a key-name (general_application_logo);

if ($request->has(self::GENERAL_APP_LOGO) && $request->$logo) {
    // Processing uploaded image here;
    $this->uploadLogo($image, self::LOGO_IMAGE_PATH, $name); // Using an upload trait

    $setting->where('key', $logo)->value = self::LOGO_IMAGE_PATH . $name; // just a try to update the DB this way
}

foreach ($keys as $key) {
    $setting->where('key', $key)->value = $request->$key; // putting all request's input values to corresponding key
}

$setting->save(); // saving the DB.

Как видите, это не сработает и вызовет исключение, например Call to undefined method ...\Eloquent\Builder::save(). Я попробовал тот же код с обновлением, но здесь сложно обновить его несколько раз (поскольку в разделе if также должно быть обновление для логотипа), а также привязать ключ к значению.

Итак, небольшая ваша помощь будет оценена по достоинству - какая здесь должна быть логика? Как я могу обновить строки БД с соответствующим значением столбца? В смысле - вот так (update where key = 'general_app_name' set value, 'some_setting_value'), но оптимизированным и понятным способом?

Рабочий раствор

Как заявил @miken32 в своем ответе, я использовал скрытую версию кода, но с небольшими изменениями:

// Changed the $request->settings->keys() to PHP native method array_keys():
$settings = Settings::whereIn('key', array_keys($request->settings))->get()->groupBy('id');
// Also, here I changed the `whereIn('id', ...)` to `whereIn('key', ...)`, since it was my primary index.

foreach ($request->settings as $k=>$v) {
  if ($k === self::GENERAL_APP_LOGO_ID) {
    // not sure about this one, but I think this is
    // how you'd access a file input in an array
    $image = $request->file('settings')[$k];
    $this->uploadLogo($image, self::LOGO_IMAGE_PATH, $name);
    $v = self::LOGO_IMAGE_PATH . $name;
  }
  // take the Setting object out of the list we pulled
  // Here I added the ->first() to get the first element from the retrieved collection;
  $setting = $settings->get($k)->first();
  $setting->value = $v;
  $setting->save();
}

Поскольку я получал значения конфигурации через помощник, который возвращает только значение текущего ключа (а не столбец id), я изменил id на key и сделал key своим ПК в модели. Работает как шарм!

Если каждый объект Setting представляет собой отдельную строку в базе данных, у вас нет другого выбора, кроме как выполнить отдельный запрос для обновления каждого из них. Я скажу, что $setting->where('key', $key)->value не сработает, потому что $setting->where('key', $key) возвращает коллекцию. $setting->where('key', $key)->first()->value может работать лучше.

miken32 12.12.2020 20:31

@miken32 miken32, и нет способа сделать его более плавным и уменьшить нагрузку на БД? Например, объединить массивы или что-то в этом роде - просто не могу сейчас придумать наиболее подходящий метод...

Max Krizh 12.12.2020 20:37
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Оживление вашего приложения Laravel: Понимание режима обслуживания
Оживление вашего приложения Laravel: Понимание режима обслуживания
Здравствуйте, разработчики! В сегодняшней статье мы рассмотрим важный аспект управления приложениями, который часто упускается из виду в суете...
Установка и настройка Nginx и PHP на Ubuntu-сервере
Установка и настройка Nginx и PHP на Ubuntu-сервере
В этот раз я сделаю руководство по установке и настройке nginx и php на Ubuntu OS.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
Как установить PHP на Mac
Как установить PHP на Mac
PHP - это популярный язык программирования, который используется для разработки веб-приложений. Если вы используете Mac и хотите разрабатывать...
0
2
1 783
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вам не нужно ставить

$setting->where('key', $logo)->value = ...;

Просто позвони

$setting->where('key', $logo)->update($request->toArray());

$настройка->сохранить(); вызывается, когда вы создаете экземпляр класса настройки, например:

$setting = new Setting();

Или

$setting = Setting::whereIn('key', $keys)->get()->first();

Затем

$setting->val = ...;
$setting->save(); // then it work's

Спасибо за ваш ответ! Ну, допустим, я делаю $setting = Setting::whereIn('key', $keys)->get()->first();, а затем $setting->value = $request->$key, это значит, что мне придется поместить запрос в цикл foreach, верно? Так же как и первая версия: $setting->where('key', $logo)->update($request->toArray()); - будет заполнять БД всеми данными формы или еще раз - foreach?

Max Krizh 12.12.2020 20:07
$setting->where('key', $logo)->update($request->toArray()); Он заполнит БД всеми данными формы. Но помните, что метод обновления будет использовать ключевое значение в качестве имени столбца и значения для обновления таблицы.
Mohammad Ali Fathi 12.12.2020 20:12

Вы можете попробовать создать текстовое поле или поле json, если ваша база данных поддерживает его, и сохранить все ваши настройки в виде строки JSON в этом поле.

идентификатор настройки 1 { "logo_path" : "img/logo.png", "foo" : "bar", "thing_count" : 17 } 2 { "logo_path" : "img/logo2.png", "foo" : "baz", "thing_count" : 4 }

В вашей модели Laravel вы можете использовать ее как массив

protected $casts = ["settings" => "array"];

а затем использовать его из модели

echo $theModel->settings['logo'];
echo $theModel->settings['foo'];

или вы можете преобразовать его как полноценный объект, если вам нужно использовать приведение значения объекта.

Одна проблема, которая может сбить людей с толку, — это установка значений в массиве для его обновления. Так не пойдет:

$theModel->settings['foo'] = "boz";

Причина в том, как работают мутаторы Laravel. Вместо этого вы делаете копию значения настроек, меняете ее и переназначаете модели:

$settings = $theModel->settings;
$settings['foo'] = "boz";
$theModel->settings = $settings;

Этот подход может бесконечно расширяться в будущем, поскольку вы просто добавляете новые ключи в свой json. Обязательно проверьте массив настроек, чтобы убедиться, что поля, которые вы ищете, установлены (именно поэтому объекты-значения могут быть очень удобными для проверки).

Это также решает вашу проблему с запросом к базе данных - она ​​всегда одна.

Спасибо за ваш ответ! Меня учили, что хранение структурированных данных в СУБД внутри Laravel — плохая практика, несмотря на то, что я применяю этот метод в другом своем проекте (на самом деле это была практика для меня, как и этот). Это усложняет такие вещи, как подсчет записей, а также увеличивает нагрузку на сервер и время отклика (поскольку сериализация и помещение данных в объект JSON — это отдельная процедура). Вот почему я пытался избежать этого способа реализации. Ничего страшного, если вы поместите его в базу данных No-SQL, например Elasticsearch и т. д.

Max Krizh 12.12.2020 21:34

Как и в большинстве случаев, это зависит от вашего варианта использования и от того, как часто вам нужно будет выполнять эти действия. Накладные расходы на обработку JSON минимальны по сравнению с накладными расходами на все SQL-запросы, если только в ваших настройках нет десятков тысяч ключей. Если вам нужно подсчитать, сколько раз появляется ключ, вы можете использовать запросы json в некоторых базах данных (или какую-либо другую форму сокращения карты). Это будет компромисс, который вам нужно будет решить для себя.

Jason 12.12.2020 22:58
Ответ принят как подходящий

С каждым параметром в отдельной строке невозможно избежать нескольких запросов к базе данных — один для получения текущих значений для всех параметров, а другой — для обновления каждого из них. Поиск элементов по первичному ключу более эффективен, поэтому я бы рекомендовал поместить содержимое столбца id в представление блейда, например так:

<label for = "setting_{{$setting->id}}">{{$setting->key}}</label>
<input name = "settings[{{$setting->id}}]" id = "setting_{{$setting->id}}" value = "{{$setting->value}}"/>

Теперь в вашем контроллере $request->settings будет массивом, который вы можете перебирать. Вы можете продолжать обрабатывать загрузку файла отдельно, но теперь у вас есть столбец id для поиска, поэтому измените свою константу на него.

$settings = Settings::whereIn('id', $request->settings->keys())->get()->groupBy('id');

foreach ($request->settings as $k=>$v) {
    if ($k === self::GENERAL_APP_LOGO_ID) {
        // not sure about this one, but I think this is
        // how you'd access a file input in an array
        $image = $request->file('settings')[$k];
        $this->uploadLogo($image, self::LOGO_IMAGE_PATH, $name);
        $v = self::LOGO_IMAGE_PATH . $name;
    }
    // take the Setting object out of the list we pulled
    $setting = $settings->get($k);
    $setting->value = $v;
    $setting->save();
}

Обратите внимание, что Laravel предлагает методы для одновременного массового обновления нескольких моделей, но они выполняют отдельные запросы к базе данных в фоновом режиме. IIRC, метод save() ничего не делает, если значение не изменилось, что избавит вас от нескольких попаданий.

Большое спасибо за ответ! Попробую реализовать этот метод. И да, я не знал, что ->save() ничего не делает, если значение не изменилось.

Max Krizh 12.12.2020 21:35

Честно говоря, не похоже, что об этом стоит беспокоиться; Я не могу себе представить, что они обновляются достаточно часто, чтобы вызвать проблемы с производительностью, поскольку обновление исходит из формы.

miken32 12.12.2020 21:56

Ну и небольшое обновление: воспользовался вашим методом, но внес в него небольшие изменения. Во-первых, я изменил $request->settings->keys() на array_keys($request->settings), а также выбрал первый элемент из данной коллекции: $setting = $settings->get($key)->first(). Теперь это работает хорошо. Единственная проблема, которая у меня есть, это то, что логотип не загружается на сервер (я использую wamp для dev env), но это история для другой проблемы > решения. Еще раз спасибо и хорошего дня! :)

Max Krizh 14.12.2020 13:43

Пожалуйста, посмотрите обновление в моем вопросе, чтобы увидеть все изменения в коде, который я сделал.

Max Krizh 14.12.2020 13:50

Другие вопросы по теме