Я пытаюсь реализовать функцию «Изменить настройки приложения». Немного подумав, мои значения конфигурации хранятся в БД со структурой ключ -> значение, например:
Как видите, для каждого параметра есть только столбец «ключ и значение». Я создал поставщика службы приложений для их кэширования навсегда и вспомогательную функцию (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 своим ПК в модели. Работает как шарм!
@miken32 miken32, и нет способа сделать его более плавным и уменьшить нагрузку на БД? Например, объединить массивы или что-то в этом роде - просто не могу сейчас придумать наиболее подходящий метод...






Вам не нужно ставить
$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?
$setting->where('key', $logo)->update($request->toArray()); Он заполнит БД всеми данными формы. Но помните, что метод обновления будет использовать ключевое значение в качестве имени столбца и значения для обновления таблицы.
Вы можете попробовать создать текстовое поле или поле json, если ваша база данных поддерживает его, и сохранить все ваши настройки в виде строки JSON в этом поле.
В вашей модели 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 и т. д.
Как и в большинстве случаев, это зависит от вашего варианта использования и от того, как часто вам нужно будет выполнять эти действия. Накладные расходы на обработку JSON минимальны по сравнению с накладными расходами на все SQL-запросы, если только в ваших настройках нет десятков тысяч ключей. Если вам нужно подсчитать, сколько раз появляется ключ, вы можете использовать запросы json в некоторых базах данных (или какую-либо другую форму сокращения карты). Это будет компромисс, который вам нужно будет решить для себя.
С каждым параметром в отдельной строке невозможно избежать нескольких запросов к базе данных — один для получения текущих значений для всех параметров, а другой — для обновления каждого из них. Поиск элементов по первичному ключу более эффективен, поэтому я бы рекомендовал поместить содержимое столбца 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() ничего не делает, если значение не изменилось.
Честно говоря, не похоже, что об этом стоит беспокоиться; Я не могу себе представить, что они обновляются достаточно часто, чтобы вызвать проблемы с производительностью, поскольку обновление исходит из формы.
Ну и небольшое обновление: воспользовался вашим методом, но внес в него небольшие изменения. Во-первых, я изменил $request->settings->keys() на array_keys($request->settings), а также выбрал первый элемент из данной коллекции: $setting = $settings->get($key)->first(). Теперь это работает хорошо. Единственная проблема, которая у меня есть, это то, что логотип не загружается на сервер (я использую wamp для dev env), но это история для другой проблемы > решения. Еще раз спасибо и хорошего дня! :)
Пожалуйста, посмотрите обновление в моем вопросе, чтобы увидеть все изменения в коде, который я сделал.
Если каждый объект Setting представляет собой отдельную строку в базе данных, у вас нет другого выбора, кроме как выполнить отдельный запрос для обновления каждого из них. Я скажу, что
$setting->where('key', $key)->valueне сработает, потому что$setting->where('key', $key)возвращает коллекцию.$setting->where('key', $key)->first()->valueможет работать лучше.