Керування відносинами в Laravel, дотримання шаблону сховищ


120

Під час створення програми в Laravel 4 після прочитання книги Т. Отвелла про хороші шаблони дизайну в Laravel я виявив, що я створюю сховища для кожної таблиці програми.

Я закінчив таку структуру таблиці:

  • Студенти: ідентифікатор, ім’я
  • Курси: ідентифікатор, ім’я, учитель_id
  • Викладачі: ідентифікатор, ім’я
  • Призначення: id, ім’я, course_id
  • Оцінки (виконує роль стрижня між студентами та завданнями): student_id, task_id, score

У мене є класи сховищ з методами пошуку, створення, оновлення та видалення для всіх цих таблиць. У кожному сховищі є красномовна модель, яка взаємодіє з базою даних. Взаємовідносини визначаються в моделі відповідно до документації Laravel: http://laravel.com/docs/eloquent#relationships .

Створюючи новий курс, все, що я роблю, - це викликати метод create у сховищі курсу. Цей курс має завдання, тому, створюючи його, я також хочу створити запис у таблиці балів для кожного студента курсу. Я роблю це через сховище призначення. Це означає, що сховище призначення призначається з двома красномовними моделями, з моделлю Assignment та Student.

Моє запитання: оскільки ця програма, ймовірно, збільшуватиметься і більше стосунків буде впроваджено, чи є гарною практикою спілкування з різними красномовними моделями у сховищах чи це потрібно робити замість інших сховищ (я маю на увазі виклик інших сховищ із сховища Assignment) ) чи це слід робити в красномовних моделях всі разом?

Крім того, чи є хорошою практикою використовувати таблицю балів як стрижень між завданнями та студентами чи це слід робити десь ще?

Відповіді:


71

Майте на увазі, що ви запитуєте думки: D

Ось моя:

TL; DR: Так, це добре.

У вас все добре!

Я роблю саме те, що ви робите часто, і вважаю, що це чудово працює.

Однак я часто організовую сховища за логікою бізнесу замість того, щоб мати репо-таблиці. Це корисно, оскільки це точка зору, орієнтована на те, як ваша програма повинна вирішити вашу "бізнес-проблему".

Курс - це "сутність" з атрибутами (назва, ідентифікація тощо) і навіть іншими суб'єктами (Призначення, які мають свої атрибути та, можливо, сутність).

Ваш сховище "Курс" має бути в змозі повернути курс та атрибути / завдання Курсів (включаючи призначення).

Ви можете досягти цього красномовно, на щастя.

(Я часто опиняюсь сховищем за столом, але деякі сховища використовуються набагато більше, ніж інші, і тому є набагато більше методів. Твоє сховище "курсів" може бути набагато більш повнофункціональним, ніж ваше сховище Assignments, наприклад, якщо ваш додаткові центри більше навколо курсів і менше про колекцію завдань курсів).

Хитра частина

Я часто використовую сховища всередині своїх сховищ, щоб зробити деякі дії з базою даних.

Будь-яке сховище, яке реалізує красномовне для обробки даних, ймовірно, поверне красномовні моделі. У цьому світлі добре, якщо модель курсу використовує вбудовані відносини для отримання або збереження Задач (або будь-якого іншого випадку використання). Наша "реалізація" побудована навколо Красномовного.

З практичної точки зору це має сенс. Ми навряд чи змінимо джерела даних на те, що красномовний не може впоратися (до не-sql-джерела даних).

ОРМИ

Найвибагливіша частина цієї установки, принаймні, для мене визначає, чи Красномовство насправді допомагає чи шкодить нам. ORM - складний предмет, оскільки, хоча вони нам дуже допомагають з практичної точки зору, вони також з'єднують ваш код "суб'єктів бізнес-логіки" з кодом, що здійснює пошук даних.

Цей вид заплутується, чи справді відповідальність вашого сховища поводиться за обробку даних або обробку пошуку / оновлення суб'єктів (суб'єктів бізнес-домену).

Крім того, вони виступають як ті самі об'єкти, які ви передаєте своїм поглядам. Якщо пізніше вам доведеться відмовитися від використання красномовних моделей у сховищі, вам потрібно переконатися, що змінні, передані вашим представленням, поводяться однаково або матимуть ті самі методи, інакше зміна ваших джерел даних перетвориться на зміну вашого поглядів, і ви (частково) втратили мету абстрагувати свою логіку в сховищах - насамперед ремонтопридатність вашого проекту знижується.

У всякому разі, це дещо неповні думки. Як це було сказано, вони є лише моєю думкою. Це трапляється результатом читання дизайну, керованого доменом та перегляду таких відео, як "дядько Боб", основний коментар на Ruby Midwest протягом останнього року.


1
На вашу думку, чи було б гарною альтернативою, якщо сховища повертають об'єкти передачі даних замість красномовних об’єктів? Звичайно, це означатиме додаткове перетворення красномовного в dto, але, принаймні, ви ізолюєте свої контролери / погляди від поточної реалізації orm.
federivo

1
Я трохи експериментував із цим і трохи виявив це з непрактичної сторони. Якщо говорити, мені ця ідея подобається в рефераті. Однак об’єкти колекції баз даних Illuminate діють так само, як масиви, а об'єкти Model діють так само, як об'єкти StdClass, достатньо, щоб ми могли, практично кажучи, дотримуватися красномовного і все-таки використовувати масиви / об’єкти в майбутньому, якщо нам потрібно.
fideloper

4
@fideloper Я відчуваю, що якщо я використовую сховища, я втрачаю всю красу ORM, яку надає красномовство. Під час отримання об'єкта облікового запису за допомогою мого методу репозиторію $a = $this->account->getById(1)я не можу просто ланцюжкові способи, наприклад $a->getActiveUsers(). Гаразд, я міг би скористатися $a->users->..., але потім я повертаю красномовну колекцію та жоден об’єкт stdClass і знову прив'язуюся до Красномовного. Яке рішення цього? Декларація іншого методу в репозиторії користувача, як $user->getActiveUsersByAccount($a->id);? Хочеться почути, як ви вирішуєте це ...
santacruz

1
ORM страшні для Enterprise (ish) -рівневої архітектури, оскільки вони викликають подібні проблеми. Зрештою, ви повинні вирішити, що має найбільш сенс для вашої заявки. Особисто під час використання сховищ з Eloquent (90% часу!) Я використовую Eloquent і намагаюся найважче ставитися до моделей і колекцій, таких як stdClasses & Arrays (адже ви можете!), Тож якщо мені потрібно, перехід на щось інше можливо.
fideloper

5
Ідіть вперед і використовуйте ліниві моделі. Ви можете змусити реальні моделі домену працювати так, якщо ви коли-небудь пропускаєте використання Eloquent. А якщо серйозно, ти збираєшся вимикач з красномовних коли - небудь? В за копійку, за фунт! (Не переходьте за борт, намагаючись дотримуватися "правил"! Я весь час ламаю).
fideloper

224

Я закінчую великий проект, використовуючи Laravel 4, і мені довелося відповісти на всі запитання, які ви зараз задаєте. Прочитавши всі доступні книги Laravel у Leanpub та тонах Гугла, я придумав таку структуру.

  1. Один клас красномовної моделі на таблицю даних
  2. Один клас сховища за красномовною моделлю
  3. Клас обслуговування, який може спілкуватися між декількома класами репозиторію.

Тож скажімо, я будую базу даних про фільми. У мене були б принаймні такі класи красномовної моделі:

  • Кіно
  • Студія
  • Директор
  • Актор
  • Огляд

Клас сховища інкапсулює кожний клас красномовної моделі та несе відповідальність за операції CRUD над базою даних. Класи сховища можуть виглядати приблизно так:

  • MovieRepository
  • StudioRepository
  • ДиректорРепозитарій
  • ActorRepository
  • ReviewRepository

Кожен клас репозиторію розширить клас BaseRepository, який реалізує такий інтерфейс:

interface BaseRepositoryInterface
{
    public function errors();

    public function all(array $related = null);

    public function get($id, array $related = null);

    public function getWhere($column, $value, array $related = null);

    public function getRecent($limit, array $related = null);

    public function create(array $data);

    public function update(array $data);

    public function delete($id);

    public function deleteWhere($column, $value);
}

Клас обслуговування використовується для склеювання декількох сховищ разом і містить реальну "ділову логіку" програми. Контролери спілкуються лише з класами обслуговування для дій "Створення, оновлення та видалення".

Отже, коли я хочу створити нову запис фільму в базі даних, мій клас MovieController може мати такі методи:

public function __construct(MovieRepositoryInterface $movieRepository, MovieServiceInterface $movieService)
{
    $this->movieRepository = $movieRepository;
    $this->movieService = $movieService;
}

public function postCreate()
{
    if( ! $this->movieService->create(Input::all()))
    {
        return Redirect::back()->withErrors($this->movieService->errors())->withInput();
    }

    // New movie was saved successfully. Do whatever you need to do here.
}

Ви самі визначаєте, як ви надсилаєте дані POST своїм контролерам, але скажімо, дані, повернені методом Input :: all () у методі postCreate (), виглядають приблизно так:

$data = array(
    'movie' => array(
        'title'    => 'Iron Eagle',
        'year'     => '1986',
        'synopsis' => 'When Doug\'s father, an Air Force Pilot, is shot down by MiGs belonging to a radical Middle Eastern state, no one seems able to get him out. Doug finds Chappy, an Air Force Colonel who is intrigued by the idea of sending in two fighters piloted by himself and Doug to rescue Doug\'s father after bombing the MiG base.'
    ),
    'actors' => array(
        0 => 'Louis Gossett Jr.',
        1 => 'Jason Gedrick',
        2 => 'Larry B. Scott'
    ),
    'director' => 'Sidney J. Furie',
    'studio' => 'TriStar Pictures'
)

Оскільки MovieRepository не повинен знати, як створювати записи Actor, Director або Studio в базі даних, ми будемо використовувати наш клас MovieService, який може виглядати приблизно так:

public function __construct(MovieRepositoryInterface $movieRepository, ActorRepositoryInterface $actorRepository, DirectorRepositoryInterface $directorRepository, StudioRepositoryInterface $studioRepository)
{
    $this->movieRepository = $movieRepository;
    $this->actorRepository = $actorRepository;
    $this->directorRepository = $directorRepository;
    $this->studioRepository = $studioRepository;
}

public function create(array $input)
{
    $movieData    = $input['movie'];
    $actorsData   = $input['actors'];
    $directorData = $input['director'];
    $studioData   = $input['studio'];

    // In a more complete example you would probably want to implement database transactions and perform input validation using the Laravel Validator class here.

    // Create the new movie record
    $movie = $this->movieRepository->create($movieData);

    // Create the new actor records and associate them with the movie record
    foreach($actors as $actor)
    {
        $actorModel = $this->actorRepository->create($actor);
        $movie->actors()->save($actorModel);
    }

    // Create the director record and associate it with the movie record
    $director = $this->directorRepository->create($directorData);
    $director->movies()->associate($movie);

    // Create the studio record and associate it with the movie record
    $studio = $this->studioRepository->create($studioData);
    $studio->movies()->associate($movie);

    // Assume everything worked. In the real world you'll need to implement checks.
    return true;
}

Тож те, що нам залишилося, - це приємне, розумне розділення проблем. Репозиторіям відомо лише красномовна модель, яку вони вставляють та отримують із бази даних. Контролерам не важливо сховища, вони просто передають дані, які вони збирають від користувача, і передають їх у відповідну службу. Службі неважливо, як отримані ними дані зберігаються в базі даних, вона просто передає відповідні дані, які їх передав контролер у відповідні сховища.


8
Цей коментар на сьогоднішній день є чистішим, масштабнішим та доцільнішим підходом.
Андреас

4
+1! Це мені дуже допоможе, дякую, що поділилися з нами! Цікаво, як вам вдалося перевірити речі всередині служб, якщо це можливо, чи можете ви коротко пояснити, що ви зробили? В будь-якому випадку, дякую Вам! :)
Пауло Фрейтас

6
Як сказав @PauloFreitas, було б цікаво подивитися, як ви обробляєте частину перевірки, і мене також зацікавить частина винятків (ви використовуєте винятки, події чи ви просто обробляєте це так, як вам здається, що ви пропонуєте у своїй контролер через бульне повернення у ваших послугах?). Дякую!
Ніколя

11
Добре запишіть, хоча я не впевнений, чому ви вводите movieRepository в MovieController, оскільки контролер не повинен робити нічого безпосередньо зі сховищем, а також ваш метод postCreate не використовує movieRepository, тому я припускаю, що ви залишили це помилково ?
davidnknight

15
Питання про це: чому ви використовуєте сховища в цьому прикладі? Це чесне запитання - мені здається, ви використовуєте сховища, але принаймні в цьому прикладі сховище насправді нічого не робить, окрім того, що забезпечує той же інтерфейс, як Красномовний, і врешті-решт ви все ще прив’язані до Красномовного, оскільки ваш клас обслуговування використовує красномовне безпосередньо в ньому ( $studio->movies()->associate($movie);).
Кевін Мітчелл

5

Мені подобається думати про це з точки зору того, що робить мій код і за що він відповідає, а не "правильно чи неправильно". Ось як я розбиваю свої обов'язки:

  • Контролери - це HTTP-рівень і спрямовує запити до базового apis (він же керує потоком)
  • Моделі представляють схему бази даних та повідомляють програмі, як виглядають дані, які зв’язки у них можуть бути, а також будь-які глобальні атрибути, які можуть знадобитися (наприклад, метод імені для повернення об'єднаного імені та прізвища)
  • Репозиторії представляють більш складні запити та взаємодії з моделями (я не роблю запитів щодо модельних методів).
  • Пошукові системи - класи, які допомагають мені будувати складні пошукові запити.

Зважаючи на це, має сенс кожен раз використовувати репозиторій (чи створюєте ви інтерфейси.etc. Це зовсім інша тема). Мені подобається такий підхід, бо це означає, що я точно знаю, куди йти, коли мені потрібно виконати певну роботу.

Я також схильний будувати базовий сховище, як правило, абстрактний клас, який визначає основні параметри за замовчуванням - в основному операції з CRUD, і тоді кожна дитина може просто розширювати та додавати методи за необхідності або перевантажувати параметри за замовчуванням. Введення вашої моделі також допомагає цій моделі бути досить міцною.


Чи можете ви показати свою реалізацію вашого BaseRepository? Я фактично це теж роблю, і мені цікаво, що ти зробив.
Odyssee

Подумайте, getById, getByName, getByTitle, збережіть тип method.etc. - загалом методи, що застосовуються до всіх сховищ у різних доменах.
Оддман

5

Подумайте про сховища як про послідовну шафу даних (не лише ваші ORM). Ідея полягає в тому, що ви хочете захопити дані в послідовному простому API.

Якщо ви виявите, що ви просто робите Model :: all (), Model :: find (), Model :: create (), ви, мабуть, не отримаєте великої користі від абстрагування сховища. З іншого боку, якщо ви хочете зробити трохи більше ділової логіки у ваших запитах чи діях, ви можете створити сховище, щоб полегшити API для роботи з даними.

Думаю, ви запитували, чи сховище буде найкращим способом вирішити справу з більш багатослівним синтаксисом, необхідним для підключення пов'язаних моделей. Залежно від ситуації, я можу зробити:

  1. Вішаючи нову дочірню модель від батьківської моделі (одна-одна чи одна-багато), я додав би метод до дочірнього сховища щось подібне, createWithParent($attributes, $parentModelInstance)і це просто додало б $parentModelInstance->idу parent_idполе атрибутів і створення викликів.

  2. Приєднуючи багато-багато стосунків, я фактично створюю функції на моделях, щоб я міг запускати $ instance-> attachChild ($ childInstance). Зауважте, що для цього потрібні наявні елементи з обох сторін.

  3. Створюючи пов'язані моделі за один запуск, я створюю те, що я називаю шлюзом (це може бути трохи відхилене від визначень Фоулера). Як я можу викликати $ gateway-> createParentAndChild ($ parentAttributes, $ childAttributes), а не купу логіки, яка може змінитися, або це ускладнить логіку, яку я маю в контролері чи команді.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.