Laravel ORM vs. raw SQL на практическом примере

Фанат

oncle terrible
Команда форума
Поднимая старую тему.
Есть такой опенсорсный словарь английского языку, WordNet, доступный, в том числе, в виде базы MySQL

Структура довольно замороченная, но нас интересуют в основном таблицы
  • words
  • senses - смыслы слов. Словарные статьи. Со словами напрямую не связаны.
  • synsets - связь между словами и их смыслом, многие ко многим: одно и то же слово может иметь несколько смыслов (например, слово "дом" имеет 17 значений), но так же при этом один и тот же смысл может быть передан несколькими словами-синонимами (максимальное количество - 28 - у слова "жопа"). Отсюда и название.
  • samples - предложения с примерами, многие к одной связке слово-смысл (senseid).
  • lexlinks - некая таблица, в которой говорится, кем какое слово приходится друг к другу. Нам отсюда нужны только антонимы.

Задача - получить в скрипте словарную статью наподобие яндексовой:
Список возможный значений, каждое со своим
- объяснением
- примерами
- синонимами
- и антонимами

И вот я сижу, морщу ум, как мне со всей этой фигней взлететь, на ларавелевой модели.
Рисовать все релейшенс в ОРМ-е я точно не хочу - это выльется в сотни запросов на одну статью.
Пока останавливаюсь на чисто raw SQL. Но даже здесь я не понимаю, где эти запросы писать? В чьей модели? Не в конроллере же!

Один из вариантов, про который я уже даже писал однажды ругательный пост - вьюхи. Вся SQL кухня во вью, а модель получается белая, пушистая и в школоладе. Но тут есть два "но":
1. Странное поведение вью:
Код:
 explain select * from samplesets where synsetid = 201793315;

+------+-------------+------------+------+---------------+------+---------+------+-------+----------------+

| id  | select_type | table      | type | possible_keys | key  | key_len | ref  | rows  | Extra          |

+------+-------------+------------+------+---------------+------+---------+------+-------+----------------+

|    1 | PRIMARY    | <derived2> | ALL  | NULL          | NULL | NULL    | NULL | 48571 | Using where    |

|    2 | DERIVED    | samples    | ALL  | NULL          | NULL | NULL    | NULL | 48571 | Using filesort |

+------+-------------+------------+------+---------------+------+---------+------+-------+----------------+
запрос ужасный. делаем

show create table samplesets;

и берем запрос оттуда:
explain  select `samples`.`synsetid` AS `synsetid`,group_concat(distinct `samples`.`sample` order by `samples`.`sampleid` ASC separator '|') AS `sampleset` from `samples` where  synsetid = 201793315 group by `samples`.`synsetid`;

+------+-------------+---------+------+----------------------------+---------+---------+-------+------+-------+

| id  | select_type | table  | type | possible_keys              | key    | key_len | ref  | rows | Extra |

+------+-------------+---------+------+----------------------------+---------+---------+-------+------+-------+

|    1 | SIMPLE      | samples | ref  | PRIMARY,k_samples_synsetid | PRIMARY | 4      | const |    2 |      |

+------+-------------+---------+------+----------------------------+---------+---------+-------+------+-------+
получаем запрос идеальный
И шо это было? Нужно использовать индекс-хинтинг?

2. Все равно одним запросом все данные не получить(?). Получается, все равно какие-то манипуляции с моделями, которые мне не очень ясны...

Вот основной запрос:
Код:
SELECT sns.synsetid,
      s.pos,
      senseid,
      definition,
      group_concat(DISTINCT sample) AS sampleset
FROM words w
JOIN senses sns ON sns.wordid=w.wordid
JOIN synsets s ON s.synsetid=sns.synsetid
LEFT JOIN samples sam ON sam.synsetid=sns.synsetid
WHERE lemma='near'
GROUP BY sam.synsetid;
- он получает смыслы и примеры.

Дальше надо получить синонимы и антонимы. Причем леммы для них. То есть, получив wordid, надо джойнить опять words.
 
Последнее редактирование:

fixxxer

К.О.
Партнер клуба
Рисовать все релейшенс в ОРМ-е я точно не хочу - это выльется в сотни запросов на одну статью.
Пока останавливаюсь на чисто raw SQL. Но даже здесь я не понимаю, где эти запросы писать? В чьей модели?
Это потому что Active Record - говно^W очень ограниченная модель.

А если не пихать работу с базой в модель, все будет хорошо. Отдельно сами модели (чистые - только инкапсуляция данных и бизнес-логики, никакой персистенции), отдельно - загрузка из базы и сохранение в оную (хотя с сохранением в AR проблем меньше).

Соответственно, не в модели и не в контроллере, а в отдельном классе (в mvc это все уровень model, а вообще это persistance layer), который сделает SQL-запрос, создаст все нужные инстансы моделей (в т.ч. по связям) и коллекции, и вернет коллекцию (collection of words в данном случае). В ларавеле так сделать особо проблем нет, разве что ручной работы многовато.
 

Фанат

oncle terrible
Команда форума
Спасибо за разумный ответ.
Правда, заимплементить его мне ума не хватит :)\

Да, а что ты думаешь насчет писать логику вовью?
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
вьюшки - это просто интерфейс доступа к данным,
запрос к базе - это не логика, это медиа-адаптер, на его месте может быть key-value или файлы,
логика для меня - это последовательность условных операторов и циклов
хочешь вьюшки - делай ради бога, они ничего не изменят в приложении, только длину строк с sql
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
строки с sql никуда не деваются, просто становятся в N раз короче,
а если у тебя, например, каталог с eav-структурой, то вьюшек не напасешься - когда фильтр включен, надо джоинить таблицу, не включен - не надо

вообще, идея выносить общий sql-код во вьюшку сама по себе интересна, можно таблицы фильтров join-ить на вьюшку,
только надо следить за версией mysql, влиянием на индексы, и кешем результата, чтоб не было внезапностей
 

fixxxer

К.О.
Партнер клуба
Да, а что ты думаешь насчет писать логику вовью?
Хак какой-то в данном контексте, да и не всегда сработает - не всё можно уложить в плоскую структуру, где одному id соответствует одна строка в result set. Произвольные строковые данные не наконкатишь через запятую, и вообще это начинает напоминать первую гостевую книгу на php =)

Кстати, use/force index mysql-вьюхи, помнится, полностью игнорировали - причем explain показывал, что все хорошо. Хотя это было давно, может, поправили.
 

Фанат

oncle terrible
Команда форума
Хак какой-то в данном контексте, да и не всегда сработает - не всё можно уложить в плоскую структуру, где одному id соответствует одна строка в result set. Произвольные строковые данные не наконкатишь через запятую, и вообще это начинает напоминать первую гостевую книгу на php =)
Ну, тут важно правильный делимитер выбрать ;)
Хак этот, кстати, мне нравится. И сам юзал, и в имеющихся в этой базе вьюхах он используется.

Кстати, use/force index mysql-вьюхи, помнится, полностью игнорировали - причем explain показывал, что все хорошо. Хотя это было давно, может, поправили.
Окак. Идея с вьюхами тускнеет на глазах. Я за нее в основном ухватился потому, что там они из коробки, основная напоминает мой запрос, но тупит на примерах - как раз из-за этого иксплейна.

В общем, в итоге я опять у разбитого корыта, и не представляю, куда что писать )
 

fixxxer

К.О.
Партнер клуба
Ну, тут важно правильный делимитер выбрать ;)
Ну я и говорю - как в первой гостевой книге: | кто-то может запостить, сделаю ||. Нет, |||. Или |/|, до такого точно никто не догадается! :)

Да может поправили давно, я-то на это лет 5 назад натыкался.
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Фанат, у тебя какая версия mysql ? :)
Но ведь в случае с AR мы получаем длину строк SQL в приложении равной нулю - вот я о чем.
как по мне, AR на вьюшку делать бесполезно - во вьюшку нет записи, лучше уж прямо entity object заполнять из результата sql-запроса

например:
PHP:
class Phrase {
  private $data = []
  public static function fillByWord($word){
    $sql = 'select * from my_phrases_view where word=:word';
    $data = DB::inst()->query($sql)->fetch();
    $obj = new self;
    $obj->data = $data;
    return $obj;
  }
  public function getSample() {
    return $this->data['sample'];
  }
}

$assWord = Phrase::fillByWord('ass');
echo $assWord->getSample;
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
вот смотри: fixxer предлагает вынести этот sql-запрос в отдельный класс, который условно называется persistance layer. В классе Phrase остается только работа с данными и обращение к persistance layer.
тогда
PHP:
class Phrase {
private $data = []
public static function fillByWord($word){
  $data = $obj = new self;
  $obj->data = Persistance::getWordData($word);
   //  или $this->serviceLocator->WordPersistanceLayer()->getWordData($word);
  return $obj;
}
я тоже так делаю. только мы этот persistance называем service, если помнишь, так повелось.

Phrase - это модель, сущность бизнес-логики. Persistance я обычно тоже кладу в папку model на yii, отдельной папки для слоя работы с БД у меня нет.
 
Последнее редактирование:

AmdY

Пью пиво
Команда форума
Это потому что Active Record - говно^W очень ограниченная модель.

А если не пихать работу с базой в модель, все будет хорошо. Отдельно сами модели (чистые - только инкапсуляция данных и бизнес-логики, никакой персистенции), отдельно - загрузка из базы и сохранение в оную (хотя с сохранением в AR проблем меньше).

Соответственно, не в модели и не в контроллере, а в отдельном классе (в mvc это все уровень model, а вообще это persistance layer), который сделает SQL-запрос, создаст все нужные инстансы моделей (в т.ч. по связям) и коллекции, и вернет коллекцию (collection of words в данном случае). В ларавеле так сделать особо проблем нет, разве что ручной работы многовато.
Да ладно, там 3-4 запроса с IN будет, что-то вроде
PHP:
Words::select(Db::raw('group_concat(DISTINCT sample) AS sampleset'))->with('senses', 'senses.sample')->where()
 

fixxxer

К.О.
Партнер клуба
Не-а, group_concat не сработает без джойна на samlpes (sample же не в таблице words). А с джойнами уже мимо AR без обработки результатов запроса.
 

флоппик

promotor fidei
Команда форума
Партнер клуба
Можно я глупый вопрос задам, а зачем там склейка group_concat если laravel умеет eager loading сохраняя структуру? Ну будет не один запрос, а три - зато простые, по которым оптимизатор всегда хорошо индексы угадывает.
 

флоппик

promotor fidei
Команда форума
Партнер клуба
https://bitbucket.org/ogorbunov/wordnet-laravel

Делает то, что тебе нужно в 10 строчек кода и 4 линейных запроса (без антонимов, правда, лень было), без единой строчки SQL, родным ORM, без единого дополнительного репозитория, barebone laravel.
Минут двадцать работы, большинство которых я потратил, пытаясь понять их связи (и возможно, где-то все равно напутал)
 

Фанат

oncle terrible
Команда форума
Мда, круто.
Реально поворачивает мозги.
Но мозги ржавые, скрипят сцуко. Никак не соображу, как сюда еще и синонимы впихнуть. SQL-ем получилось - надо всего лишь приджойнить senses на себя еще раз по synsetid - то есть, получить все слова с тем же смыслом.
Код:
SELECT sns1.synsetid,
      sns1.sensenum,
      s.pos,
      posname,
      sns1.senseid,
      definition,
      group_concat(DISTINCT sample) AS sampleset,
      group_concat(DISTINCT w2.lemma) synonyms
FROM words w1
JOIN senses sns1 ON sns1.wordid=w1.wordid
JOIN synsets s ON s.synsetid=sns1.synsetid
JOIN postypes p ON p.pos=s.pos
LEFT JOIN samples sam ON sam.synsetid=sns1.synsetid
LEFT JOIN senses sns2 ON sns1.synsetid=sns2.synsetid
AND sns2.wordid!=w1.wordid
LEFT JOIN words w2 ON w2.wordid = sns2.wordid
WHERE w1.lemma='good'
GROUP BY sam.synsetid
ORDER BY sns1.sensenum;
 

Фанат

oncle terrible
Команда форума
С антонимумами
Код:
SELECT sns1.synsetid,
      sns1.sensenum,
      s.pos,
      posname,
      sns1.senseid,
      definition,
      group_concat(DISTINCT sample) AS sampleset,
      group_concat(DISTINCT w2.lemma) synonyms,
      group_concat(DISTINCT w3.lemma) antonyms
FROM words w1
JOIN senses sns1 ON sns1.wordid=w1.wordid
JOIN synsets s ON s.synsetid=sns1.synsetid
JOIN postypes p ON p.pos=s.pos
LEFT JOIN samples sam ON sam.synsetid=sns1.synsetid
-- synonyms
LEFT JOIN senses sns2 ON sns1.synsetid=sns2.synsetid
    AND sns2.wordid!=w1.wordid
LEFT JOIN words w2 ON w2.wordid = sns2.wordid
-- antonyms
LEFT JOIN lexlinks l ON l.synset1id=sns1.synsetid
    AND word2id != w1.wordid
    AND linkid=30
LEFT JOIN words w3 ON w3.wordid = word2id
WHERE w1.lemma='good'
GROUP BY sam.synsetid
ORDER BY sns1.sensenum;
 

флоппик

promotor fidei
Команда форума
Партнер клуба
Фанат, по их структуре получается, что у слова нет синонимов, так? а есть слова с этим же смыслом? Но у слова то много смыслов, значит и синонимы у каждого смысла, верно? Если я это правильно понял, тогда просто:
PHP:
class Sense extends Eloquent { ... public function synonyms() {return $this->hasMany('sense', 'synsetid', 'synsetid');}}


Код обновил.
 
Сверху