forum.boolean.name

forum.boolean.name (http://forum.boolean.name/index.php)
-   Алгоритмика (http://forum.boolean.name/forumdisplay.php?f=21)
-   -   Чат: PHP + MySQLi или что то другое? (http://forum.boolean.name/showthread.php?t=20044)

St_AnGer 17.10.2015 11:32

Чат: PHP + MySQLi или что то другое?
 
Добрый день, Булка!

В целях развития решился я тут снова приняться за чатик аля Аська или (если брать из известного среди ньюфагов) Viber. Ну как чатик - полноценную систему обмена сообщениями с сохранением всего и вся чем обмениваются пользователи. Этот же чат будет и для яОСи (где то на Булке я уже выкладывал скрин меню по яОСь), и, в последующем, наверно, под Ведроид. Но не суть. Все наработки которые у меня были под яОСь были мной отвергнуты ввиду своей убогости и неоправданной сложности. В общем, начинаю с нуля.

Не спрашивайте про распространения/раскрутки, "да кому он нужен" и т.п. вопросы. Не будут пользоваться - хрен бы с ним, мне важно обучиться. А лучше суровой практики в этом деле нету ничего.

На данный момент пишу веб-клиент (так как его куда проще реализовать, чем приложение под мобилки) и общий сервер на всё. Что есть в планах на реализацию:
- регистрация
- авторизация
- активация пользователя пятью голосами других пользователей или одним голосом администратора (изначально планировалось как корпоративный чат, посему лишних болтунов в нём не надо)
- просмотр списка неактивированных пользователей
- поиск пользователей
- добавление пользователей в список
- удаление пользователей из списка
- добавление групп пользователей (привет, Аська!)
- изменение групп пользователей (пока что только изменение названия группы)
- перенос пользователей из контактного списка в созданные группы
- удаление групп (вместе со всеми пользователями что в ней состоят)
- создание бесед
- запрос списка бесед в которых состоит пользователь
- отсылка сообщений в беседы
- запрос списка сообщений в беседе

Сейчас имею реализованные с лёгкостью и полностью оттестированные первые 11 пунктов, уткнулся на создании бесед.
Беседы будут создаваться двумя путями:
1) беседа из отдельных выбранных пользователем участников;
2) беседа с выбранной группой пользователей.
С этим проблем нет, приведение к общему списку пользователей на серверной стороне я реализовал. Но, беседы ведь не должны дублироваться, вот тут у меня и начались сложности. Нужно перед созданием новой беседы произвести поиск уже существующей, по сформированному массиву пользователей и, исходя из результатов, или вернуть id старой беседы, или создать новую.


Вот конкретно что не знаю и не очень понимаю.

Пункт 1.
Каким образом организовать структуру таблиц базы данных для этого?
Есть 2 мысли:
а) таблица "chats" в которой есть поле owner (собственно тот кто создал) и поле users (в ней хранится сортированный json-массив id пользователей состоящих в диалоге)
б) таблица "chats" (есть аналогичное поле owner) и таблица "chat_users" (содержит записи о пользователях в конкретных беседах)

Чем нравится вариант "а":
- нет лишней таблицы
- данные о беседе и список её пользователей хранятся в одной записи, простой поиск по id беседы
- довольно простой поиск беседы зная только список пользователей из ней (сформировали json-массив и спокойно ищем по полю "users")
Чем не нравится вариант "а":
- "сложность" извлечения списка пользователей. Сложность в том, что сначала надо декодировать массив пользователей и только потом делать запрос к базе данных что бы узнать их имена и данные для отображения
- вытекающая из предыдущего проблема поиска списка бесед в которых состоит текущий пользователь

Чем нравится вариант "б":
- простота поиска бесед в которых состоит текущий пользователь
- простота извлечения (одним запросом) данных пользователей зная id беседы (SELECT по id беседы и пара JOIN-ов для данных пользователей)
Чем не нравится вариант "б":
- очень сложный и долгий поиск беседы зная только список пользователей из неё

Извлечение данных пользователей беседы так же важно, как и поиск самой беседы по списку пользователей (как писал выше - такой поиск необходим при создании беседы).


Пункт 2.
Как организовать саму переписку в беседе? Допустим, как добавить сообщение в базу данных понятно - INSERT и всё. Но как реализовать правильную выборку сообщений для пользователей состоящих в беседе? Если в беседе 1 пользователь - всё просто, один запрос. А если 10 пользователей - то это уже 10 запросов за одними и теми же данными. Вопрос скорости работы при 30-40 пользователях встанет ребром, а при 100 и более - сервер умрёт. Расширение мощности сервера не вариант - чаты существуют очень давно и изначально они были реализованы на машинах с мощностью меньшей, чем нынешние калькуляторы. Однако работало всё на ура.

В наличии VPS, его конфигурация такова: 2 ГГц проц, 1 ГБ оперативы, 10 гигов пространства на HDD, Debian в качестве ОСи, никаких "иксов", только консоль. Серверную сторону пишу на PHP + MySQLi (возможно заменю на Firebird SQL, очень уж я к нему прикипел на предыдущей работе).

Буду очень рад советам по этому направлению, потому что знаний по нему у меня чуть больше чем ноль. Читал статьи на хабре, много думал, рисовал разных схем, искал структуру и принцип работы Аськи, ВКонтакта, Фейсбука.

Сегодня узнал про некий Redis (нереляционная БД в ОЗУ), но как его готовить и с чем его употреблять - пока что представления не имею, хотя её данные мне очень понравились, особенно скорость работы (естественно она в десятки раз быстрее, чем любая хранимая на HDD база данных). Интересно было бы узнать мнение работавших/работающих с ним. И ещё было бы интересно узнать как из Redis в MySQLi данные перекочевряжить (лично мне удобнее читать из базы данных данные, чем из файлов "снимков" данных из ОЗУ), хотя скрипт переноса на том же PHP я скорее всего тоже накатаю довольно быстро.

h1dd3n 17.10.2015 16:56

Ответ: Чат: PHP + MySQLi или что то другое?
 
Во-первых с точки зрения удобства пользования:

Зачем нужно проверять беседу при создании на "дубликацию" ?
Если в скайпе ты создал конференцию A в которой есть Ты, Вася и В.В. Путин, то при создании конференции Б в которой есть все те же товарищи не должно быть ошибки - это же 2 разные конференции, и там сообщения разные. Или если у тебя есть 2 друга и вы вместе сидите в конференции "Беседка C++" (вас там только 3) и также вы сидите в конференции "Беседка PHP" (вас там тоже только 3) никаких проблем в этом нету. 2 разные конференции - 2 разные чата (хоть и набор пользователей идентичен).

И к тому же групп. чаты (беседы, комнаты, конференции) как правило не создаются с указанием пользователей сразу - ты сначала создаешь чат "Обсуждение Fallout 4", а потом туда пользователей добавляешь (или удаляешь).

Во-вторых насчет структуры в БД:

Если у тебя реляционная база, то классическим вариантом будет users, chats, chat_user_links. В users - пользователи, в chats - чаты, в chat_user_links связи пользователей и чатов (в минимальном варианте 2 колонки chatId, userId).
В этом варианте у тебя:
Быстрый поиск всех пользователей в чате.
Быстрый поиск всех чатов у пользователя.
Что еще нужно?

В-третьих насчет "переписки":

Сама переписка к базе никакого отношения не имеет.
Переписка это получение и доставка сообщений (как 1х1 так и 1хдохрена).
И тут может быть (на факт что обязательно будет, но может появится) проблема в PHP. Весь вопрос в том как ты собираешься в php держать соединения с пользователями (само tcp соединение конечно держит http сервер, но суть в том как устроена обработка событий с этих соединений). Есть разные подходы, и если на малом кол-ве клиентов они все дают одинаковый результат то при увеличении некоторые варианты дают совсем херовый результат (тут тебе придется изучить технологию на которой ты пишешь)

Когда ты написал про базу, я так понимаю ты имел в виду "где и как хранить историю? и каким образом отдавать ее юзерам?".
В целом ответ 1 - хранить историю на клиенте (это можно сделать и веб приложении, а в мобильном/десктопном и подавно). Если прям нужно чтобы и на сервере хранилась, то записывать и на сервер и на клиент. Тогда с сервера тянуть историю будут только те у кого ее по каким-то причинам нет (только что присоединившийся пользователь, пользователь который у себя историю потер и т.д.). В этом случае нагрузка будет уже совсем не большая.

moka 18.10.2015 04:42

Ответ: Чат: PHP + MySQLi или что то другое?
 
Много текста..

Для чата нужен real-time - это главное.
Следственно делать чат на PHP + MySQL - это как делать ММО на PHP, т.к. клиенту нужно будет постоянно спрашивать о сообщениях - AJAX. Постоянные HTTP запросы - не нужный overhead.
Это можно обойти Long Poll'ом или WebSocket'ами, но читай дальше.

PHP + MySQL. Как понимаю MySQL будет использоваться посредником для хранения данных и обменом между запросами к PHP. В любом случае PHP скрипту прийдется постоянно спамить MySQL на наличие новых сообщений. При каждом запросе от клиента или при фиксированном периоде если используется Long Poll или WebSockets подход.
Короче - постоянные запросы в огромные таблицы MySQL - просто не нужный overhead снова.

Как делается чат:
Начинается все только с real-time основы. Тебе нужно позаботиться о двух вещах:
1. real-time рассылка с минимальным overhead'ом.
2. Горизонтальное масштабирование, т.к. при больших нагрузках одного сервера тебе никогда не хватит.

1 - решается очень просто - не используй никаких посредников, а используй pub/sub паттерн, и при получении сразу рассылай клиентам которые подписаны на канал (комнаты), или шли напрямую на соединения (их может быть несколько), клиенту которому адресовано сообщение.

2 - сложнее. Тут у тебя может быть много процессов, с большим числом клиентов каждый. Тебе нужно сделать систему routing'а сообщений, т.к. иногда у тебя может оба общающихся клиента быть в одном процессе, а могут быть на разных. В таком случае нужно делать Routing систему, где сообщения будут толкаться серверам которым нужно пересылать сообщения. Использовать одну точку для messaging pub/sub, например redis - не смасштабируется при огромной нагрузки, но хватит на очень много. Рассматривай варианты где каждый сервер будет узнавать о разных изменениях стейтов клиентов, или использовать routing карту чтобы узнать списки серверов (может быть несколько снова), исходя из пользователя которому шлем или комнаты.
С комнатами лучше всех клиентов комнаты на одном сервере держать - так проще.
Также можно использовать MQ систему: RabbitMQ, ZeroMQ и т.п.

Только затем идет сохранение сообщений, и история.
"недавняя" история, типо последние 10 сообщений нужно держать в памяти все равно на серверах, чтобы избежать необходимости обращаться к базе данных при новых клиентах для получения недавних сообщений.
История отдельно сохраняется на сервере и никак не влияет на систему рассылок и т.п.

Далее тебе нужно будет позаботиться о push notifications системе. Учитывая что пользователь может быть на нескольких клиентах залогинен сразу (web и android например). А может быть и не одном из них.
Тут нужно быть окуратным. Если ты общаешься в web'е нормально, а на android у тебя клиент выключен, тебе не нужно слать push notification на android, т.к. пользователь уже прочитал сообщение в web'е.


Если честно, делать такое на PHP + MySQL это подход ранних 2000, это как делать ММО на PHP.
Не стоит.
Используй для этого подходящие инструменты: erlang, redis, cassandra.
Прототип чата на node.js + sockjs пишеться буквально за 20 минут.
Тебе не нужня реляционная база данных тут, лучше использоваь NoSQL который хорошо подходит к огромным объемам данных (чат - это много реал-тайм данных).

Также хранить историю переписки на серверах - учитывай что это весьма серъезное решение. Тут могут быть легальные и этические вопросы.

St_AnGer 18.10.2015 23:26

Ответ: Чат: PHP + MySQLi или что то другое?
 
Много полезной информации почерпнул, перепланировал сильно дальнейшую разработку, спасибо вам!

PHP + MySQL были выбраны из за того, что ими я владею на уровне среднего писаря (в отличии от всех остальных технологий для вебчика и серверов). Так то хочется чего то нового и более подходящего, как раз Redis хочу попробовать.

Но и от MySQL не хочу полностью отказываться, потому что мне с ней общаться удобно (на данный момент), в ней можно хранить самих пользователей и историю сообщений, а онлайн-часть держать в той же Redis, изредка (раз в 3-5 минут) сбрасываю инфу в MySQL. Мне кажется это удобным (ну, опять же на данный момент и с моими текущими знаниями). На счёт "моральных" проблем хранения истории - это не мои проблемы. Вайберу можно, а мне нет? :-) На самом деле первоначальным условием написания чата было поставлено именно хранение истории.

До node.js всё никак не доберусь, надо его пощупать будет, и, возможно им и пользоваться для организации самого api, а не старым-добрым пыхом. Просто на данный момент я вообще не представляю что это такое, как и с чем его едят, есть ли под него библиотеки для "Push Notifications", в общем - ничего о нём не знаю.

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

Горизонтальное масштабирование - штука крутая, но при серьёзных нагрузках. У меня в чате, если его реально распространить по предприятию, будет 200-300 человек. Думаю такую нагрузку при грамотной организации даже PHP+MySQL потащит на нынешнем железе. Но это мелочи, надо делать сразу с масштабируемостью, согласен. Даже если не взлетит - опыт будет получен колоссальный.

UPD собственно, только что сбацал на VPS себе node.js, начинаю изучать что к чему. Одна беда - пых я знаю, а node.js в глаза не видел (js конечно умею, но не на столько, чтоб сервер писать).

moka 19.10.2015 01:08

Ответ: Чат: PHP + MySQLi или что то другое?
 
По node'у полным полно примеров, простых модулей и т.п.
Главное начать.

То что будет 200-300 человек, ты не упомянул однако ранее :) или я не дочитал :D
Тогда да, можно чатик за пару дней с клиентами и всеми фичами написать. Или даже что-то готовое поднять.
Push Notifications - не нужно использовать когда клиент открыт, т.к. он должен соединяться.
PN - нужен только если клиент вырублен и пользователь не был активен на других клиентах. ~5 сек задержки после активности все же нужно, чтобы избежать ситуации где прочитал в вебе, а он еще и на мобилку пушает.

Если речь идет о 200-300 человек, то и redis не нужен. Поднимай один процесс и храни все в памяти.
Бд чисто для аутентификации и хранения истории.
С nodejs mongodb идет очень легко и гладко. Ты пример какой-нибудь глянь, там все просто.
PHP код:

var mongo = require('mongodb').MongoClient;
var 
url 'mongodb://localhost:27017/mydb';

mongo.connect(url, function(errdb) {
    if (
err) throw err;
    
console.log("connected to db");

    
db
    
.collection('users')
    .
findOne({
        
login'user',
        
password'hash'
    
}, function(erruser) {
        if (
err) throw err;

        if (! 
user)
            return 
console.log('wrong login / password');

        
console.log(user);
    });
}); 

Соединение конечно держать нужно одно, а не много (как в пхп). Ну и тут пример как найти поьзователя с бд.
Крч, копай и пробуй, примеров в интернете куча, и на форуме помогу если где-то застопоришься.

St_AnGer 19.10.2015 09:36

Ответ: Чат: PHP + MySQLi или что то другое?
 
Цитата:

Сообщение от moka (Сообщение 300617)
То что будет 200-300 человек, ты не упомянул однако ранее :) или я не дочитал :D
Тогда да, можно чатик за пару дней с клиентами и всеми фичами написать. Или даже что-то готовое поднять.

Сколько будет человек - дела не меняет. Хочу сделать всё грамотно, что бы в теме понимать, и, что бы работало как надо если вдруг будет решено запускать это дело в ход. Чужое (готовое) можно поднять конечно, это проще, но... Мне надо самому научиться, яжпрограммист! :)

Node.js поднял, кое как стартовал на субдомене своём. Можешь посоветовать, нормальный ли способ я выбрал для этого (оно взлетело и работает, но всё же)? Порт 3000-й выбран осознанно, субдомен api2 тоже осознанно выбран (на api висит текущая версия на php+mysql).
Код:

var subdomain = require('express-subdomain');
var express = require('express');
var app = express();

var router = express.Router();

router.get('/', function(req, res) {
        res.send('trololo');
});

app.use(subdomain('api2', router));
app.listen(3000);

UPD Прототип "чата" на node.js + socket.io написал реально за 30 минут, из которых 20 изучал что такое node.js и socket.io. Учимся дальше...

UPD2 Грусть пичаль таска абида. Mongo DB не взлетает на VPS основанной на OpenVZ (установлена Debian 8.2).

UPD3 после плясок с бубном - Mongo DB взлетела. Учимся дальше...

moka 19.10.2015 15:18

Ответ: Чат: PHP + MySQLi или что то другое?
 
Цитата:

Сообщение от St_AnGer (Сообщение 300618)
Сколько будет человек - дела не меняет. Хочу сделать всё грамотно, что бы в теме понимать, и, что бы работало как надо если вдруг будет решено запускать это дело в ход. Чужое (готовое) можно поднять конечно, это проще, но... Мне надо самому научиться, яжпрограммист! :)

Учиться - верно, всегда хорошо. Но многие вещи не нужно делать до того как у тебя будет в них необходимость, иначе и не дойдешь до первого релиза.

Цитата:

Сообщение от St_AnGer (Сообщение 300618)
Node.js поднял, кое как стартовал на субдомене своём. Можешь посоветовать, нормальный ли способ я выбрал для этого (оно взлетело и работает, но всё же)? Порт 3000-й выбран осознанно, субдомен api2 тоже осознанно выбран (на api висит текущая версия на php+mysql).
Код:

var subdomain = require('express-subdomain');
var express = require('express');
var app = express();

var router = express.Router();

router.get('/', function(req, res) {
        res.send('trololo');
});

app.use(subdomain('api2', router));
app.listen(3000);

UPD Прототип "чата" на node.js + socket.io написал реально за 30 минут, из которых 20 изучал что такое node.js и socket.io. Учимся дальше...

UPD2 Грусть пичаль таска абида. Mongo DB не взлетает на VPS основанной на OpenVZ (установлена Debian 8.2).

UPD3 после плясок с бубном - Mongo DB взлетела. Учимся дальше...

У тебя nginx или apache стоит на сервере? Если nginx, то там легко делается прокси, по субдомену, таким образом порт 3000 в ссылке не нужно будет иметь.

St_AnGer 19.10.2015 16:00

Ответ: Чат: PHP + MySQLi или что то другое?
 
Цитата:

Сообщение от moka (Сообщение 300630)
Учиться - верно, всегда хорошо. Но многие вещи не нужно делать до того как у тебя будет в них необходимость, иначе и не дойдешь до первого релиза.

У тебя nginx или apache стоит на сервере? Если nginx, то там легко делается прокси, по субдомену, таким образом порт 3000 в ссылке не нужно будет иметь.

У меня довольно жестоко сделано - связка nginx+apache. Nginx под статику, apache под динамику. Но основной Nginx. Собственно, уже сделал чтоб порт не прописывать, за наводку спасибо, что то даже не подумал про это :)

moka 19.10.2015 16:34

Ответ: Чат: PHP + MySQLi или что то другое?
 
Для прокси WebSocket трафика там доп. настройки нужны тоже:
PHP код:

http {
    
upstream chatsrv {
        
server 127.0.0.1:3000;
    }

    
server {
        
listen 80;
        
server_name sub.domain.com;

        
location / {
            
proxy_pass          http://chatsrv/;

            
proxy_set_header    Host $host;
            
proxy_set_header    Upgrade $http_upgrade;
            
proxy_set_header    Connection "upgrade";
            
proxy_set_header    X-Real-IP $remote_addr;
            
proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
            
proxy_set_header    X-NginX-Proxy true;
        }
    }



St_AnGer 20.10.2015 09:35

Ответ: Чат: PHP + MySQLi или что то другое?
 
И внезапно возник вопрос. Пока я не углубился в кущи MongoDB хотелось бы узнать, а подходит ли РЕАЛЬНО документарная база данных для сложного чата ("чат" звучит очень грубо, скорее микрокопия фейсбука без ленты активности, о чём я, разумеется забыл упомянуть выше... да хотя сейчас даже старая-добрая Аська уже не просто банальная чатилка, какой была раньше)? Ведь у меня будут пользователи и информация о пользователях (которая возможно будет меняться), будут группы и информация о группах (которая возможно будет меняться), в конце концов будут сообщения, которые будут содержать отправителя (информация о котором будет будет возможно меняться).

В общем, проведя глубокий анализ и прочитав статью про "Диаспору" (вот прям только что) уже не очень уверен, что MongoDB правильный выбор для меня. Даже банальный (банальный ли?) MySQL больше подходит своей реляционностью, лично мне так кажется. А MongoDB больше подходит под справочники (аля Кинозал) своей документарностью. Если же делать примерно что запланировал я, то будет очень много дублирования данных в тех же беседах и сообщениях (сообщение ведь содержит отправителя, а это значит что и всю инфу о нём, если следовать парадигме денормализации данных). А в случае если отправитель ВНЕЗАПНО решит поменять инфу о себе (добавил/изменил ник/дату рождения/пол/да что угодно о себе), то ПРИДЁТСЯ менять всю эту инфу во всех документах где этот пользователь фигурирует. Можно конечно хранить ссылки на пользователей в беседах и сообщениях, это позволит избежать дублирования данных, но абсолютно уничтожит все полюсы документарной СУБД превратив её в реляционную (причём ещё и с отсутствием "аппаратных" джоинов, что приводит к "программным" джоинам в коде).

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

Пойду почитаю про PosgreSQL...

Кстати, понял что я пытался сделать в первой версии своего "чата". Я пытался сделать что то среднее между реляционной и документарной структурой данных , что бы если надо было добавить к какой то записи ещё одно поле - ненадо было добавлять его ко всем записям (используя MySQL хранил, например, в сообщениях JSON о отправителе с краткой информацией, которую можно было просто расширить добавив конкретно в это сообщение в JSON нужное поле), и как раз наткнулся на беду, описанную мной выше... Невозможность человеческого изменения данных, простого, без тучи кода по изменению ВСЕХ мест где используются эти данные.

pax 20.10.2015 10:52

Ответ: Чат: PHP + MySQLi или что то другое?
 
Это какие такие дублирования данных? Можно же id записей одной коллекции хранить в полях другой. Запросы реляционные не сделать это да, но а часто это не нужно. Получил список сообщений, получи для них пользователей отдельным запросом.

PS: даже не думай о дублировании данных. Такой подход в любой СУБД не правильный.

St_AnGer 20.10.2015 11:28

Ответ: Чат: PHP + MySQLi или что то другое?
 
Цитата:

Сообщение от pax (Сообщение 300648)
Это какие такие дублирования данных? Можно же id записей одной коллекции хранить в полях другой. Запросы реляционные не сделать это да, но а часто это не нужно. Получил список сообщений, получи для них пользователей отдельным запросом.

Можно то можно, но в чём тогде смысл документарной базы данных, если мы из неё делаем реляционную (не избыточную и целостную), просто на "программном" уровне (в коде приложения), а не на аппаратном (в архитектуре самой СУБД). Причём вот такой запрос в той же MySQL
Код:

SELECT a.id,
          a.name
      FROM chat AS a
      LEFT JOIN chat_users AS b ON b.chat_id = a.id
  WHERE b.user_id = 1

(поля chat - id, name;
поля chat_users - id, chat_id, user_id)

Отработает горррраздо быстрее и проще, чем я сначала отдельным запросом запрошу документ пользователя с user_id = 1, а вторым запросом я вытащу документ чата, в котором найду "чаты" данного пользователя.
Я почему то твёрдо уверен что документарная структура мне не очень хорошо подходит.
Вот картинка (с хабра, не моя), описывает банальную ситуацию с лайками и коментами (я конечно не делаю лайки, но ситуация схожа):


То что выделено зелёным - имеет один и тот же тип данных (то есть users). То есть, что бы достать инфу о всех лайках и комментариях у пользователя, надо:
- или хранить полностью всю инфу о самих комментерах и лайкерах следуя парадигме документарной СУБД, что бы все данные вытаскивать одним документом (дублирование (читай денормализация, избыточность данных) данных + чревато если лайкер/комментер, например, сменил какие то свои данные, например имя);
- или хранить ключи (id) на самих комментеров и лайкеров, но тогда придётся писать свой собственный "программный" JOIN, что является лишним гемороем и убивает основную фичу документарной СУБД - скорость за счёт доставания инфы одним документом (то есть попытку приведения документарной СУБД к недореляционной).

Цитата с хабра (перевод статьи про Диаспору), как раз именно так я и понимаю использование MongoDB для моей задачи:

Цитата:

Примерно так выглядит плотностью денормализованная лента активности.


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

....

Существует другой подход к решению проблемы в MongoDB, который будет знаком тем, кто имеет опыт работы с реляционными СУБД. Вместо дублирования данных вы можем сохранять ссылки на на пользователей в ленте активности.

При этом подходе вместо встраивания данных там, где они нужны, вы даете каждому пользователю ID. После этого вместо встраивания данных пользователя вы сохраняете только ссылки на пользователей. На картинке ID выделены зеленым:

(MongoDB фактически использует идентификаторы BSON — строки, похожие на GUID. На картинке числа, чтобы легче было читать.)

Это исключает нашу проблему дублирования. При изменении данных пользователей, есть только один документ, который нужно изменить. Тем не менее, мы создали новую проблему для себя. Потому что мы больше не можем построить ленту активности из одного документа. Это менее эффективное и более сложное решение. Построение ленты активности в настоящее время требует, чтобы мы 1) получить документ ленты активности, а затем 2) получили все документы пользователей, чтобы заполнить имена и аватары.


Ну, то есть, на реляционной СУБД конкретно такая задача не проблема вообще (заводим несколько таблиц и джоинами вытаскиваем нужные данные). Мой личный вывод по MongoDB - крутая хранилка не связанных друг с другом JSON-данных (аля медиатека). Если данные в двух разных хранилищах связаны какими то ключами друг с другом - значит мы используем MongoDB как реляционную СУБД, то есть не по назначению. Вот так я понимаю работу с MongoDB.

зыЖ Я ни в коем случае не говорю что MongoDB это плохо (во первых, - я с ней знаком всего 2 дня, во вторых - да кто я такой, что бы это говорить) и скорее всего не понимаю всех плюсов документарной СУБД и буду рад если мне кто нибудь примерно зарисует/распишет как надо правильно им пользоваться в моём направлении. Полную структуру что должно храниться и в какие моменты извлекаться могу расписать.
На любой SQL-СУБД реализую всё что мне надо довольно просто и быстро, с сохранением целостности и нормализации данных в случае какого либо изменения этих данных, чего абсолютно не гарантирует денормализованная структура (а если делать нормализованную структуру - то смысл в MongoDB для этой задачи отпадает).

pax 20.10.2015 11:53

Ответ: Чат: PHP + MySQLi или что то другое?
 
ИМХО всякие вконтакты и фейсбуки достают дополнительную инфу о лайках по необходимости отдельными AJAX запросами. В момент, когда ты наводишься на кнопку лайка. Получать их одним запросом и перегружать страницу избыточными данными не нужно. Для отображения списка сообщений тебе достаточно два запроса: сами сообщения + их авторы. Всю дополнительную инфу можно получить позже отдельными запросами в момент, когда они понадобятся. В самих сообщениях можно хранить счетчики лайков и только.

PS: А вообще я понял кажется твое сообщение. Зачем хранить в базе данных то, в каком чате какой пользователь? Как сказал мока, это реалтаймовая фигня. Ее надо хранить в памяти сервера, а не в базе. При отсылке сообщения в комнату сервер будет в базу только писать само сообщение и не будет выдергивать из нее пользователей, кому это сообщение нужно доставить. Доставляется сообщение только подключенным пользователям.

moka 20.10.2015 14:10

Ответ: Чат: PHP + MySQLi или что то другое?
 
pax правильно говорит - сообщению не нужно знать имя пользователя. Нужно знать только ID.
Если client-1 не знает о user-1, то это забота сервера рассказать ему о user-1. Таким образом client-1 без проблем должен просто получать сообщения с userId, которые это уже работа клиента заменять на username.

Тут не нужна реляционная работа, и не нужны никакие join'ы.
Все должно быть примитивно и связано на ID.
Слать username с сообщениями - просто не нужно. Тем более избыточно.

St_AnGer 20.10.2015 15:19

Ответ: Чат: PHP + MySQLi или что то другое?
 
Принцип понял, наверно.

Сейчас, распишу что понял и задам вопросы, если не затруднит - ответьте и поправьте ошибки в логике.
Базу буду использовать примерно как использовал бы MySQL (без избыточной информации, с ссылками по id).
Для переписки достаточно будет 2 коллекции - users и chats.
Коллекция Users - содержит инфу для авторизации и вообще инфу о пользователях.
Коллекция Chats - содержит сами чаты, в которых имеются подколлекции users (id состоящих в них пользователей) и messages (собственно сообщения со всеми нужными данными).
Можно сделать ещё третью коллекцию - Attachments (передача файлов тоже планируется, каждый аттач будет связан со своим сообщением).

Имеем (допустим идеальный случай) 3 зареганых пользователя - A, B, C). Каждый пользователь сидит ТОЛЬКО С ОДНОГО устройства и никак иначе (идеальные условия).
Пользователь C находится офлайн.
Пользователь A создаёт беседу "Room1" и приглашает в неё пользователей B и C.
Комната прилетает на сервер и записывается в БД. Начало общения в ней положено.

Вопрос 1. Как оповестить пользователей о том что они приглашены в беседу?
Как вижу я - если пользователь держит постоянный коннект с сервером то в целом просто, по этому коннекту пользователя и оповещаем.
И от сюда вопрос 2. Как быть если пользователь офлайн?
Как вижу я - при подключении пользователя (переход в онлайн) обращаться с запросом "очереди событий" (которую надо как то содержать), от куда и получает приглашение в беседу

Далее. Пользователи приглашены в беседу, всё хорошо.
Пользователь A отсылает в беседу сообщение.
Сообщение прилетает на сервер, записывается в базу к беседе (для истории). Попутно оно отсылается через коннект пользователю B.
Пользователи A и B общаются между собой, пользователь C всё ещё офлайн.
Через какое то время пользователь C таки появляется онлайн и входит в данную беседу.

Вопрос 3 и 4. Как пользователю получить историю сообщений? Когда у него нет истории всё просто - скачиваем что есть (грубо всё, или лимитировано по 10-20 сообщений со смещениями). А что делать если пользователь вошёл в диалог, какое то время там был, а потом стал оффлайн? То есть, что ему отдавать когда он снова станет онлайн? Всю историю беседы точно отдавать ненадо (она ведь уже есть), но существуют некоторые пробелы, которые надо заполнить. Можно конечно забить болт на это дело и пойти по принципу "пропустил - твои проблемы", но не хотелось бы, ведь во всяких вконтактах и вайберах же нету такого "болтозабивательства", вайбер так вообще очень слаженно скачивает историю. То есть, если общался на мобилке, то на компе всегда появляется достоверная без пробелов история, если запустить приложение после переписки на мобилке (и наоборот соответственно тоже работает), проверял специально.

И вопрос 5. Вычитал что у MongoDB ограничение на коллекцию 16 мегабайт. Что делать когда коллекция chats достигнет этого предела (16 мегабайт не так то и много)? Надо как то будет распределять данные в другие коллекции?

moka 20.10.2015 15:49

Ответ: Чат: PHP + MySQLi или что то другое?
 
Если тебе нужно бращаться к данным которые являются суб-документом, то лучше не сувать все в один документ, а разбить по коллекциям. Поэтому лучше так:
users - юзеры
rooms - комнаты, содержит массив users - что является массивом id пользователей.
messages - сообщения.
notifications - разного рода оповещания пользователям. Например что их пригласили в комнату или в чат с другим пользователем.

Предположим такой пример документа messages:
PHP код:

{
    
_id32,
    
user16,
    
room64,
    
timestampISODate("2015-10-20T11:43:18.915Z"),
    
data'hello world!'


Для переписки 1-1 я бы тоже создавал room.

Можно содержать также у room'а, время последнего сообщение.
Таким образом если кто-то пришел в чат и версия его кеша отличается, то клиент запросит у сервера данные недавней истории сообщений, также раскажет о времени в кеше.
Естественно сервер не должен высылать сразу всю историю, а должен слать только недавние скажем 32 сообщения. Далее если клиент продолжает скроллить, и ему нужно еще сообщений, то запросит еще 32.
Сортировка тут по времени, следственно просто запрашиваешь типо такого:
PHP код:

db.messages.find({
    
'room'64,
    
'timestamp': {
        
$ltISODate("2015-10-20T11:56:07.516Z")
    }
}).
sort({ timestamp}).limit(32); 

Вот так получишь последние 32 сообщения, до определенной даты.

Индексировать коллекции естественно нужно тоже как полагается.
Например messages индексируй так:
PHP код:

db.messages.ensureIndex({
    
'room'1,
    
'timestamp': -1
}); 

Давай больше экспериментов, и начинай с попроще, не замудряй :)

pax 20.10.2015 16:04

Ответ: Чат: PHP + MySQLi или что то другое?
 
16 мб на одну запись же.

moka 20.10.2015 16:37

Ответ: Чат: PHP + MySQLi или что то другое?
 
Цитата:

Сообщение от pax (Сообщение 300658)
16 мб на одну запись же.

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

St_AnGer 21.10.2015 17:33

Ответ: Чат: PHP + MySQLi или что то другое?
 
НЕОЖИДАННЫЙ вопрос.

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

Задача: достать список друзей и групп одним api-вызовом.
Что имеем:
- 3 коллекции:
users - пользователи (требуемые поля _id, first_name, last_name)
friends - список друзей каждого пользователя (что то типа связывающей таблицы многие-ко-многим, содержит поля group_id, owner и friend, где group_id - _id группы в которую пользователь запихнул этого друга, owner - _id самого пользователя-владельца, friend - _id друга (по сути - _id другого пользователя))
groups - список групп

В общем виде вот так выглядят записи в этих хранилищах:

Users:
Код:

{
  "_id" : ObjectId("56263bd1bc9082875487820b"),
  "first_name" : "Вячеслав",
  "last_ip" : "",
  "last_name" : "Дядчиков",
  "online" : 1,
  "online_date" : ISODate("2015-10-21T07:49:30.93Z"),
  "register_date" : ISODate("2015-10-20T13:04:17.814Z"),
  "sex" : 1,
  "state" : 2
}

Friends:
Код:

{
  "owner" : ObjectId("56263bd1bc9082875487820b"),
  "friend" : ObjectId("562743368a654b61739a7b27"),
  "group_id" : 1,
  "state" : 1,
  "add_date" : ISODate("2015-10-21T11:43:17.498Z"),
  "_id" : ObjectId("56277a55f472bd707e67f2d7")
}

Groups:
Код:

{
  "_id" : 1,
  "name" : "Без группы",
  "owner" : 0,
  "state" : 1,
  "add_date" : ""
}



Достать список групп - легко.
Достать просто список друзей (чисто список _id друзей) - легко.
Достать расширенный список друзей - что то никак не получается. Ну, точнее получается, но... Сейчас опишу ситуацию.
Достать всё это сразу и отправить пользователю - вообще никак не получается.

Клиент делает вызов api "блаблабла/friends.get". Передаётся _id пользователя запрашивающего список. Должен вернуться JSON содержащий массив friends (одна запись содержит поля _id,first_name,last_name,group_id) и массив groups (одна запись содержит поля _id, name).

Соответственно как я вижу решение:
1) Берём из коллекции friends нужные записи по owner, и по этим записям берём из коллекции users данные (first_name, last_name).
2) Берём из коллекции groups нужные записи по owner
3) формируем JSON и отправляем в ответ на вызов API

На какие проблемы я наткнулся:
1) не пойму как вызывать функции ПОСЛЕДОВАТЕЛЬНО, т.е.
что бы функция поиска групп вызвалась сразу после окончания поиска друзей.
Сделал так:
Код:

db.collection('friends').find({owner:id,state:{$gt:0}}).toArray(friendCallback);
db.collection('groups').find({state:{$gt:0},$or:[{owner:id},{owner:0}]}).toArray(groupCallback);
res.end(JSON.stringify({
                        data: [{
                                type: 0,
                                code: 0,
                                friends: friends,
                                groups: groups
                        }],
                        error: null
                }));

Но, оно выполняется сразу, вызывается сначала поиск в друзьях, потом не дожидаясь отрабатывания коллбэка вызывается поиск в группах, и , не дожидаясь отрабатывания коллбэка отправляется ответ.
Пытался делать выхов в коллбэке, тогда работает, но код выглядит просто ужасно (ещё ужаснее чем сейчас).

2) связанно с первым, так как в функции friendCallback я делаю ещё один запрос для раширенных данных:
Код:

function friendCallback(err, results){
        for (var i = 0; i < results.length; i++) {
                db.collection('users').find({_id:new ObjectID(results[i].friend)}).toArray(friendCallback1);
        }
}

И оно конечно же тоже не успевая отрабатывать перелетает по циклу, а ведь в friendCallback1 как раз происходит формирования массива friends, который потом отдаётся в ответ.

Сейчас в результате всего этого дела пользователю приходит ответ с пустыми массивами.
Код:

{"data":[{"type":0,"code":0,"friends":[],"groups":[]}],"error":null}
На SQL такое решается элементарным join:
Код:

SELECT        u.id,
                u.firstName,
                u.lastName,
                f.f_groupId
        FROM users AS u
        LEFT JOIN friends as f ON f.friend = u.id
        WHERE f.state = 1 AND f.owner = ?

Поиск вменяемого ответа не дал, да хотя я и ищу скорее всего не правильно (потому что ищу что то в стиле "как сделать join в node.js+mongodb"). Могу это реализовать тремя разными вызовами API (написав их, конечно же), но надо что бы всё прилетало в одном вызове.

зыЖ наверно это выглядит как тупое клянчанье "напишите за меня код", но нет, код не прошу. Прошу теорию, которую я, увы, не знаю.

moka 22.10.2015 03:54

Ответ: Чат: PHP + MySQLi или что то другое?
 
Ой. Ты совсем не туда "пошел".

Во первых - асинхронное программирование, это концепт который ты видимо пока не понял.
Суть заключается в том что у тебя есть только один поток выполнения кода. И есть асинхронный паттерн, где ты типо говоришь: сделай А и когда будет готов ответ, вызови функцию Б.
Таким образом естественно у тебя два вызова и ответ на запрос будут обработаны сразу один за другим, не дожидаясь callback'ов.
Это не PHP, где каждый вызов блокируется - что на самом деле очень плохо.

Во первых делать API запросы которые возвращают много разной информации - очень плохо.
Лучше сделать два запроса. Таким образом в разных частях кода, когда тебе нужны будут первые или вторые данные, ты сможешь использовать уже имеющийся API route.

Пару советов по дизайну API:
1. Запросы должны быть конкретными и простыми.
2. Любой запрос должен выдавать нормальные ошибки и учитывать возможность не валидных данных от клиента.
3. Консистентный формат ответа - также помогает упростить реализацию API и клиента.

Вот пример с двумя частями.
Первая часть - это middleware для аутентификации по token'у пользователя - распространенный метод аутентификации с API. Этот токен не должен знать только клиент, и создается при аутентификации по логину паролю. Он также может иметь срок годности после последнего использования. При logout'е, token также должен удаляться.
Каждая аутентификация должна создавать свой новый token. Это по сути token клиента/сессии.
По token'у мы востанавливаем данные о пользователе. Далее имея пользователя, мы можем выполнять запрос от него.
Т.к. этот middleware загружает пользователя нам, удобно сделать список полей которые мы запрашиваем, чтобы не делать дополнительный запрос после.

Вторая часть кода, пример как получить список друзей и выслать ответ в виде JSON'а.
Подрузумевается что список ID храниться в массиве friends у пользователя.
Таким образом запросить список его друзей, очень просто.

PHP код:

var userAuthMiddleware = function(args) {
    var 
fields = { _id};
    if (
args.fields) {
        for(var 
0args.fields.lengthi++) {
            
fields[args.fields] = 1;
        }
    }

    return function(
reqresnext) {
        if (! 
req.query.token)
            return 
next(new Error('missing token'));

        
db.collection('auth').findOne({
            
_idreq.query.token
        
}, function(errtoken) {
            if (
err) return next(err);

            if (! 
token)
                return 
next(new Error('not authenticated'));

            
db.collection('users').findOne({
                
_idtoken.user
            
}, {
                
fieldsfields
            
}, function(erruser) {
                if (
err) return next(err);

                
// user might be not found
                
if (! user)
                    return 
next(new Error('user not found'));

                
// user logged in
                // store in req object so other middleware can access it
                
req.user user;

                
// call next middleware
                
next();
            });
        });
    }
};

// friends
app.get('/friends'userAuthMiddleware({
    
fields: [ 'friends' ]
}), function(
reqresnext) {
    
// find friends if have any
    
if (req.user.friends.length) {
        
db.collection('users').find({
            
_id: {
                
$inreq.user.friends // we store friend ids in user's array field `friends`
            
}
        }, {
            
fields: {
                
name// we need to get only their name
            
}
        }).
toArray(function(erritems) {
            if (
err) return next(err);
            
// response with array of users
            
res.json(items);
        });
    } else {
        
// if no friends, just response with empty array
        
res.json([ ]);
    }
}); 

Также лучше использовать численные ID, нежели ObjectID, т.к. они слишком большие и с ними геморойнее работать.
Также тебе нужно позаботиться в будущем о pagination - страницы, для infinite scrolling или просто системы страниц, т.к. если у кого-то слишком много друзей, грузить один большой список - это может быть не слишком хорошо.

ИМХО, ты опять спешишь, и забегаешь слишком вперед.
Старайся начинать с более простых примеров и задачек, и экспериментов.

St_AnGer 22.10.2015 09:36

Ответ: Чат: PHP + MySQLi или что то другое?
 
Цитата:

Сообщение от moka (Сообщение 300709)
Ой. Ты совсем не туда "пошел".

Во первых - асинхронное программирование, это концепт который ты видимо пока не понял.
Суть заключается в том что у тебя есть только один поток выполнения кода. И есть асинхронный паттерн, где ты типо говоришь: сделай А и когда будет готов ответ, вызови функцию Б.
Таким образом естественно у тебя два вызова и ответ на запрос будут обработаны сразу один за другим, не дожидаясь callback'ов.
Это не PHP, где каждый вызов блокируется - что на самом деле очень плохо.

С асинхроном у меня плохо, согласен. Пользовался им только для Ajax и то в простой форме - запрос сделали, потом когда то пришёл ответ и отреагировали асинхронно на него.

Цитата:

Сообщение от moka (Сообщение 300709)
Во первых делать API запросы которые возвращают много разной информации - очень плохо.
Лучше сделать два запроса. Таким образом в разных частях кода, когда тебе нужны будут первые или вторые данные, ты сможешь использовать уже имеющийся API route.

Собственно сейчас уже "временно" так и сделал - два отдельных API-запроса. Но тут дело в том, что эти два вызова связаны (вывод списка пользователей не возможен без вывода списка групп), поэтому и хотел сразу в одном запросе получиться все данные.

Цитата:

Сообщение от moka (Сообщение 300709)
Пару советов по дизайну API:
1. Запросы должны быть конкретными и простыми.
2. Любой запрос должен выдавать нормальные ошибки и учитывать возможность не валидных данных от клиента.
3. Консистентный формат ответа - также помогает упростить реализацию API и клиента.

1. Все запросы стараюсь делать конкретными (кроме этого как раз), да. За основу для себя взял список API Вконтакте, так как все его API-запросы соответствуют моим (friends.get, friends.add, user.signup, user.auth, friends.search и т.д.). Они довольно прозрачны.
2. Это прям сразу реализовал, до этого вёл довольно "крупный" проект по размещению данных в социальные сети (работал в рекламном агенстве, для чего писалось своё API над разными API социальных сетей (facebook, twitter, instagram, vk.com, livejournal) и целых ботов для некоторых соц.сетей (мой мир, одноклассники, google+). Поэтому ошибки которые были допущены там стараюсь не повторять и сразу предугадывать.
3. Это тоже стараюсь делать сразу, все ответы сводятся к JSON-массиву обязательно выглядящему либо так (отправляются запрошенные данные):
Код:

{
        data: {
                type: 0,
                code: 02,
                ...
        },
        error: null
}

либо так (отправляется ошибка)
Код:

{
        data: null,
        error: {
                code: 123,
                msg: ...
        }
}

Цитата:

Сообщение от moka (Сообщение 300709)
Также лучше использовать численные ID, нежели ObjectID, т.к. они слишком большие и с ними геморойнее работать.

В таком случае надо их генерировать самому? Просто не копал в эту сторону пока что, использовал что есть. ObjectID на данный момент мне не понравились своей нечитабельностью.

Цитата:

Сообщение от moka (Сообщение 300709)
Также тебе нужно позаботиться в будущем о pagination - страницы, для infinite scrolling или просто системы страниц, т.к. если у кого-то слишком много друзей, грузить один большой список - это может быть не слишком хорошо.

Вот собственно и хотел после "перевода" сервера (с PHP+MySQL на node.js+mongodb, благо реализовано пока что всего 15 только самых нужных API, реализую их постепенно по мере модификации грубого наброска веб-чатика для первой версии сервера) заняться pagination, потому что с проблемой её отсутствия уже сталкивался при первом подходе к чату.

Цитата:

Сообщение от moka (Сообщение 300709)
ИМХО, ты опять спешишь, и забегаешь слишком вперед.
Старайся начинать с более простых примеров и задачек, и экспериментов.

Вот прям в точку. Просто хочется (мне самому от себя) чтоб было сделано ещё вчера, поэтому спешу, лечу и пропускаю повороты. Из за этого часто возвращаюсь назад. Есть такая проблема у меня, трудно с этим спорить. Борюсь с ней потихоньку :-D. Дело ещё и в том, что на пыхе всё что нужно на данном этапе могу реализовать сходу, а вот на js "влоб" не получается. Языки то, по сути и подходу к ним - разные. И как бы получается что "я хочу, я могу, я умею, но не этой отвёрткой и не этот шуруп". Не сошлось, короче :). Ну это ничего, свыкнусь, освоюсь.

За код спасибо, он для меня местами выглядит страшно, но вполне читаемо и понятно. Меня прёт в "реляционность", хоть тресни. Ещё не освоился с удобствами монги, поэтому сделал отдельную коллекцию friends со связками (users связаны с users в отношении многие-ко-многим через коллекцию friends), заместо того, чтобы хранить список id друзей у самого пользователя. Меня несёт не туда, а нода и монга попутно ещё и ломают мои взгляды на программирование в целом :-D.

moka 22.10.2015 13:59

Ответ: Чат: PHP + MySQLi или что то другое?
 
Цитата:

Сообщение от St_AnGer (Сообщение 300713)
С асинхроном у меня плохо, согласен. Пользовался им только для Ajax и то в простой форме - запрос сделали, потом когда то пришёл ответ и отреагировали асинхронно на него.

Привыкнешь, и потом будешь использовать "силу асинхронности" в свою пользу.


Цитата:

Сообщение от St_AnGer (Сообщение 300713)
В таком случае надо их генерировать самому? Просто не копал в эту сторону пока что, использовал что есть. ObjectID на данный момент мне не понравились своей нечитабельностью.

ObjectID имеет выгоду тем что он гарантированно уникален при наличии нескольких баз данных (кластеризация). Но это также обходиться и вообще редко нужно. Я использую отдельную коллекцию, с ID - именем коллекции которой генерим ID, и value - увеличивающимся числом.
Код простой - атомарный запрос с увеличением счетчика. Также вставит новую запись если ее уже нету:
PHP код:

db.collection('ids').findAndModify({
    
_id'users'
}, { }, {
    
$inc: {
        
value1
    
}
}, {
    
upserttrue,
    new: 
true
}, function(erritem) {
    if (
err) return next(err)
        
    
id item.value;
    
    
// ...
}); 

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

Цитата:

Сообщение от St_AnGer (Сообщение 300713)
Вот собственно и хотел после "перевода" сервера (с PHP+MySQL на node.js+mongodb, благо реализовано пока что всего 15 только самых нужных API, реализую их постепенно по мере модификации грубого наброска веб-чатика для первой версии сервера) заняться pagination, потому что с проблемой её отсутствия уже сталкивался при первом подходе к чату.

pagination на самом деле просто делается.
Я использовал middleware паттерн у express'а для этого.
В общем запросы: app.get('/path', middlewareA, middlewareB, ..);
Идея в том что middleware может быть сколько угодно, и они будут запускаться по очереди. Чтобы соблюдать очередь уважая возможную асинхронность в них, нужно самому вызывать next метод в каждом middleware. При этом можно вызвать next(new Error('...')); что закончит очередь, и вызовет middleware для ответа с ошибкой.
Это удобно как в примере выше я привел для аутентификации например.
Также и для pagination.
Можно сделать что-то типо:
PHP код:

var pagination = function(args) {
    
args args || { };
    
args.skip args.skip || 0;
    
args.limit args.limit || 16;
    if (
args.sort) {
        var 
key args.sort;
        
args.sort = { };
        
args.sort[key] = args.order || -1;
    } else {
        
args.sort = { _id: -};
    }

    return function(
reqresnext) {
        var 
obj = { };

        
obj.skip parseInt(req.query.skip10);
        if (
isNaN(obj.skip))
            
obj.skip args.skip;

        
obj.limit parseInt(req.query.limit10);
        if (
isNaN(obj.limit))
            
obj.limit args.limit;

        
obj.sort args.sort;

        
req.pagination obj;

        
next();
    }
};

app.get('/friends',
    
userAuth({ // auth
        
fields: [ 'friends' ]
    }),
    
pagination({ // pagination
        
sort'name',
        
order: -1,
        
skip0,
        
limit16
    
}),
    function(
reqresnext) { // route controller
        
if (req.user.friends.length) {
            
db.collection('users').find({
                
_id: {
                    
$inreq.user.friends
                
}
            }, {
                
fields: {
                    
name1
                
}
            })
            .
sort(req.pagination.sort// there needs to be a sorting
            
.skip(req.pagination.skip// how many items to skip
            
.limit(req.pagination.limit// how many to return
            
.toArray(function(erritems) {
                if (
err) return next(err);
                
res.json(items);
            });
        } else {
            
res.json([ ]);
        }
    }
); 

И этот middleware можно переиспользовать в других API запросах естественно.

Цитата:

Сообщение от St_AnGer (Сообщение 300713)
Вот прям в точку. Просто хочется (мне самому от себя) чтоб было сделано ещё вчера, поэтому спешу, лечу и пропускаю повороты. Из за этого часто возвращаюсь назад. Есть такая проблема у меня, трудно с этим спорить. Борюсь с ней потихоньку :-D. Дело ещё и в том, что на пыхе всё что нужно на данном этапе могу реализовать сходу, а вот на js "влоб" не получается. Языки то, по сути и подходу к ним - разные. И как бы получается что "я хочу, я могу, я умею, но не этой отвёрткой и не этот шуруп". Не сошлось, короче :). Ну это ничего, свыкнусь, освоюсь.

Да мне тоже так хочется часто, жаль что приходится постепенно :)

Цитата:

Сообщение от St_AnGer (Сообщение 300713)
За код спасибо, он для меня местами выглядит страшно, но вполне читаемо и понятно. Меня прёт в "реляционность", хоть тресни. Ещё не освоился с удобствами монги, поэтому сделал отдельную коллекцию friends со связками (users связаны с users в отношении многие-ко-многим через коллекцию friends), заместо того, чтобы хранить список id друзей у самого пользователя. Меня несёт не туда, а нода и монга попутно ещё и ломают мои взгляды на программирование в целом :-D.

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

St_AnGer 26.10.2015 09:51

Ответ: Чат: PHP + MySQLi или что то другое?
 
Вложений: 1
И тут неожиданно возникла странность. Нужно сделать удаление пользователей из списка, и казалось бы всё просто, но...

В общем, опишу сначала добавление друзей в список контактов. Происходит оно у меня так:
ищем друга через поиск, жмакаем кнопку "добавить", обращаемся к соответствующему API где происходит добавление друга в массив friends текущего пользователя. В базе о друзьях хранятся следующие поля:
1) _id - _id существующего в базе пользователя, который является друга
2) group_id - _id группы которую создали мы (группы - что то типа групп из Аськи, просто группировка контактов как удобнее, что бы можно было писать сразу всей группе, не создавая комнату и не добавляя туда всех вручную)
3) state - состояние друга в списке (0 - удалён, 1 - в списке)
4) add_date - дата добавления

Добавление происходит двумя путями:
1) добавляемого друга нету и никогда не было в списке - всё просто, делаем Push в массив friends с нужными полямиь, state, соответственно, равен 1;
2) друг уже когда то был в спиcке друзей, но мы его удалили и сейчас в массиве friends у него state равняется 0 - тут в целом тоже просто, хотя пришлось поразвлекаться в итоге свёл всё к одному "запросу" в базу:
Код:

collection.update({_id:owner}, {$push:{friends:{_id:friend, group_id:1,state:1,add_date:addDate}}});
group_id = 1 это общая группа с названием "Без группы".

Так вот, этот этап я прекрасно пережил, всё шикарно и жизнь вроде бы удалась, удаление пользователя из списка друзей не должно стать какой то проблемой, но... Есть одно требование, которое надо было выполнить, что я и сделал. Заключается оно в том, что один друг может у текущего пользователя числиться сразу в нескольких группах. Это я реализовал тоже довольно просто - ещё один push в массив friends, с group_id нужной группы, всё работает. Соответственно с этого момента появляется 2 пути удаления:
1) полное удаление из списка (легко и не принуждённо ставим state = 0 всем текущим записям с _id друга в массиве friends пользователя)
2) удаление из какой то определённой группы (вот тут то и возникла проблема)

Контактный список выглядит сейчас вот так:
Вложение 22003

Проблема с удалением заключается в том, что когда я нажимаю удалить выделенного пользователя (Алексея) - у меня удаляется (state = 0) ПЕРВЫЙ пользователь в этой группе (то есть удаляется Людмила, а не Алексей).

Запрос на удаление вот такой:

Код:

collection.update({_id:owner,"friends._id":friend,"friends.group_id":group_id}, {$set:{"friends.$.state":0}})
Такое поведение начинается если указывать _id группы из которой удаляем ("friends.group_id":group_id) в выборке на update. Если _id группы не указать - удаляется именно Алексей, но из первой группы (что вполне логично - туда ведь он был добавлен раньше, чем в Group 1, и обновляется как раз эта запись). И вот я не очень понимаю как эту проблему порешить, вообще не понимаю. Почему он обновляет запись не с нужными _id друга, я же явно указываю его _id ("friends._id":friend), и он точно верный? Ощущение что вообще полностью игнорируется моё конкретное указание _id друга, поэтому и обновляется первая запись с нужным group_id.

В чём парадокс - добавление в группу у меня происходит почти таким же запросом (в случае когда друг был в списке, но потом мы его удалили и теперь надо просто поставить state = 1):
Код:

collection.update({_id:owner,"friends._id":friend,"friends.group_id":group_id}, {$set:{"friends.$.state":1,"friends.$.add_date":addDate}});
И ОНО РАБОТАЕТ!Не работает, сейчас перепроверил. До этого обновлял сам специально первого пользователя в списке (в данном случае удалял и добавлял Людмилу), в этом случае всё работает. А вот если я в базе сам ручками поставлю Алексею state = 1 в группе Group 1 (Алексей идёт второй записью с данной группой) и потом попробую его добавить - происходит точно тоже самое что и при удалении, обновляется первая запись с данной группой (то есть обновляется Людмила). WTF?
Я понимаю что проблема скорее всего в неправильном использовании мной вот этой конструкции:
Код:

"friends.$.state":1
, конкретно $. Но до введения правила "один пользователь в многих группах" оно работало как надо - и добавляло, и удаляло как надо. Проблемы начались когда начал указывать group_id в update.

На php+mysql, кстати, работает :-) Но там я делал несколько по другому, ведь была отдельная таблица friends, соответственно там надо было просто найти запись с нужным friend_id и group_id, и, поставить ей state = 0.

У меня есть вариант как это сделать на ноде+монго чтоб 100% работало, но мне от чего-то кажется что он слегка черезжопный. Заключается вариант в том, что надо не пытаться апдейтить элемент массива friends в базе, а вытащить этот массив, "вживую" его проапдейтить и записать полностью на место старого. Реализовал сейчас временно такое "удаление", работает. Но не верю что нельзя вот так апдейтить как я выше пытался, явно должен быть способ.

moka 26.10.2015 15:01

Ответ: Чат: PHP + MySQLi или что то другое?
 
Добавление group_id для меня кажется сомнительной затеей, по пару причинам:
1. friends содержит больше информации чем просто друзья
2. разные места хранения group_id, что тоже не збсь
3. Возможная дупликация данных, и необходимость это все менеджить.

По этому упрости лучше себе жизнь, и в friends тупо храни список друзей.
Если проще делать, то просто храни массив ID чтобы было проще индексировать.
Если таки хочешь объект держать, то не используй массив, а используй реальный объект:
PHP код:

friends: {
    
friend_id: {
        
addedDate,
        
state1
    
}
}; 

friend_id - реальный ID друга.
Но все равно тебе прийдется иметь отдельно массив со списком всех ID в нем для индексации, т.к. индексировать key'и объекта нельзя.
В итоге дополнительное поле будет:
PHP код:

friend_ids: [ friend_id_1friend_id_2 ]; 

Когда у тебя будет такой формат данных, тебе будет в разы проще обновлять какие-то данные конкретного друга, либо удалить его сразу.

Ты изначально себе усложнил задачу используя массив.

Также не храни там group_id, храни их в одной коллекции отдельно, чтобы было в разы проще оперировать этим делом.

Обновить стейт друга 64 у пользователя 32:
PHP код:

db.users.update({
    
_id32
}, {
    
$set: {
        
'friends.64.state'0
    
}
}); 

Удалить друга:
PHP код:

db.users.update({
    
_id32
}, {
    
$unset: {
        
'friends.64'1
    
},
    
$pull: {
        
'friend_ids'64
    
}
}); 


St_AnGer 27.10.2015 11:27

Ответ: Чат: PHP + MySQLi или что то другое?
 
Цитата:

Сообщение от moka (Сообщение 300857)
Обновить стейт друга 64 у пользователя 32:
PHP код:

db.users.update({
    
_id32
}, {
    
$set: {
        
'friends.64.state'0
    
}
}); 

Удалить друга:
PHP код:

db.users.update({
    
_id32
}, {
    
$unset: {
        
'friends.64'1
    
},
    
$pull: {
        
'friend_ids'64
    
}
}); 


Как это сделать в БД я понял, а как мне в скрипте обратиться к тому же другу 64, если я знаю только переменную содержащую это значение? Ну то есть, я руками вставил друга 64, а дальше?

Код:

friend = 64; //64 нам пришло из POST запроса.

db.collection('users', function(err, collection){
        collection.find({_id:owner}).toArray(function(err, results){
                if (results[0].friends.friend) {
                        ...
                }
        });
});

results[0].friends.friend естественно не может найти поля friend.
А обратиться так надо кровь из носу.



И вставлять как? Вставка в массив происходила до безобразия просто $push-ем:
Код:

collection.update({_id:owner}, {$push:{friends:{_id:friend, group_id:0,state:1,add_date:addDate}}});
Как же быть с объектом?

И ещё - тип "объект" в данном случае будет эквивалентен типу "документ"?

АПД с созданием объекта я протупил что то. Объект в js этож как ассоциативный массив:
Код:

friends[64] = {...}
Надо иногда отдыхать...

Остался вопрос грамотной вставки нового друга в базу. И вопрос доступа. Я же владею только переменной id-друга которая пришла в запросе, надо как то собрать в этом вот куске
Код:

collection.update({_id:owner}, {$set:{'friends.64.state':0}});
строку 'friends.64.state' имея переменную friend = 64. Пока что не пойму как.

moka 27.10.2015 19:12

Ответ: Чат: PHP + MySQLi или что то другое?
 
Вставлять:

PHP код:

var data = { };
data['friends.' id] = {
    
state0
};

collection.update({
    
_idowner
}, {
    
$setdata,
    
$addToSet: {
        
friend_idsid
    
}
}); 

Обновлять стейт:
PHP код:

var data = { };
data['friends.' id '.state'] = 0;

collection.update({
    
_idowner
}, {
    
$setdata
}); 

Если используешь node 4+ то там есть computed properties, часть ecma6, и можно просто так:

PHP код:

collection.update({
    
_idowner
}, {
    
$set: {
        [
'friends.' id '.state']: 0
    
}
}); 


St_AnGer 29.10.2015 09:36

Ответ: Чат: PHP + MySQLi или что то другое?
 
Спасибо тебе Максим огромное! Ежели бы не ты, сидел бы мой сервачок на пэхапэ и дальше :)
В общем то, всю "ненужную" часть чатилки я реализовал (ну там добавление в друзья, группы, перенос в группы, удаление, просмотр информации, голосование за принятие в сеть и т.п.). Теперь нужно организовывать сами комнаты и переписку в них. Собственно, пока что я для реализации "онлайн"-переписки остановился на библиотеке socket.io, понравилась она мне своей простотой и мощью.

Для начала, потренироваться в обращении с библиотекой, я решил сделать некое подобие онлайн-оффлайн для пользователей как это было в Аське. Т.е. вошёл - тебе присвоился и отправился всем кому надо статус "онлайн", вышел - соответственно "оффлайн". И вот у меня возник вопрос - а как отправлять данные на нужные сокеты (т.е. только тем у кого ты в друзьях)?

Я сейчас реализовал это ровно так, как вижу сам.

Для начала, моё "api" разделилось на 2 части - часть основанная на чистой node.js (всякая мишура для обвязки чата) и часть использующая socket.io (онлайн-офлайн, комнаты и переписка).

Далее, я каждому пользователю добавил "скрытое" поле in_friend, которое содержит всех кто добавил его в друзья. Сделал я это исключительно из соображений скорости - пробежаться по одному массиву/объекту явно быстрее, чем перебрать всю базу и посмотреть у кого же наш вошедший пользователь в друзьях. Собственно, тут всё просто. А вот дальше началась жесть.

Для того что бы запоминать сокеты для клиентов я создал массив sockets, в который вносятся новые сокеты или обновляются старые при обращении через socket.io. Т.е. массив не содержит данных больше, чем должен содержать. Сравнение "старый-новый" происходит по моему собственному полю _id, которое я внёс в объект session у сокетов. Если сокет с сессией содержащей _id уже есть - заменяем на новый, на данном этапе это не критично, потом, конечно, может не кисло так аукнуться (в конце расписал почему), но будем разбираться с проблемами по мере их поступления, сейчас не об этом.

Так вот, собственно в сокетах реализовал два события, online и offline, которые берут из базы инфу о пользователе (id пользователя передаётся вместе с событием online/offline), достают из поля in_friend все id тех у кого пользователь состои в друзьях, потом я пробегаюсь по массиву с друзьями и внутри по массиву с сокетами, найдя нужный сокет отправляю соответствующее событие тем кого найду в этом массиве. Смущает один момент. Пока количество сокетов крайне мало (я вот имею возможность тестить на 5-6 машинах), перебор массива не представляет опасности. А если сокетов в массиве будет тысяча? Не уничтожит ли это мой сервер, если одновременно событие online/offline передадут хотя бы 20 человек, каждый из которых, допустим, состоит в друзьях у 40 человек? Ведь это уже будет фактически 40*1000*20 = 800000 циклов с отправкой данных по сокетам, что просто нереально дофига на такое маленькое событие как передача статуса online/offline. И точно такое же количество циклов будет и на приглашение в комнату, и на отправку сообщения. Т.е. нагрузка будет очень и очень недурственная только из за такого моего подхода к сокетам.

зыЖ Подсчёт количества циклов очень грубый, у меня стоит break в цикле с сокетами после совпадения _id и отправки статуса. Так как у меня сокеты с _id пользователя не повторяются, то этот подход с выходом из цикла вполне применим, как мне кажется. Но этот же подход накладывает очень существенное ограничение - один пользователь будет сидеть только с одного клиента, причём где зашёл последним, там и будет работать всё, старые сокеты будут замещены новым.

зыыЖ Забыл упомянуть, изредка клиентом посылается "alive"-пакет (раз в 60 секунд сейчас сделал), который записывает отославшему пользователю статус online в базе данных и записывает время получения этого пакета. При запросе списка друзей другими пользователями (у кого в списке есть наш пользователь) идёт проверка дат пакетов "alive", если разница между текущей датой и датой "alive" > 60 секунд - пользователю в базе записывается статус offline. Работает :) Но, опять же, вопрос возможной нагрузки стоИт остро.

зыыыЖ Сейчас пришла в голову идея хранить все живые сокеты пользователя в базе данных, допустим в поле sockets. Это должно избавить от перебора общего огромного массива сокетов, ведь нужные сокеты (а сокет же в данном случае простой JS объект?) мы сможем достать только лишь опираясь на массив in_friends текущего пользователя. Надо будет попробовать так сделать.

moka 29.10.2015 18:07

Ответ: Чат: PHP + MySQLi или что то другое?
 
Данные о сокетах, хранить нужно только в ОЗУ node.js сервера. Бд об этом знать не нужно вообще.

Вот я написал пример кода, как хранить в ОЗУ список пользователей, их списки соединений и кэш друзей и т.п.
Твоя задача следить за кешем, чтобы он был всегда обновлен. Когда делаешь обновления через API добавляя/убирая там друзей, важно держать кеш обновленным тоже.

По сути users кеш - это все пользователи онлайн.
т.к. они хранятся по ключу, то проверка на онлайн и доступ к ним очень быстрый.

Вот код, думаю идея понятна.
PHP код:

// users cache object
var users = { };
// users load requests queue
var userRequests = { };


// user class
function User(data) {
    
this.id data.id;
    
this.name data.name;
    
this.friends data.friend_ids;
    
this.sockets = { };
    
this.connections 0;
}

// send message to all user connections
User.prototype.send = function(namedata) {
    for(var 
key in this.sockets)
        
this.sockets[key].emit(namedata);
};

// add user connection
User.prototype.socketAdd = function(socket) {
    if (
this.sockets[socket.id])
        return 
false;

    
this.sockets[socket.id] = socket;
    
this.connections++;

    return 
true;
};

// remove user connection
User.prototype.socketRemove = function(socket) {
    if (! 
this.sockets[socket.id])
        return 
false;

    
delete this.sockets[socket.id];
    
this.connections--;

    return 
true;
};


// try get or load user
var userGetOrLoad = function(idfn) {
    if (
users[id])
        return 
fn(nullusers[id]);

    
// loading in progress, just queue then
    
if (userRequests[id])
        return 
userRequests[id].push(fn);

    
// first one to try loading
    
userRequests[id] = [ fn ];

    
// once loaded, report to all who need this info
    
var finish = function(erruser) {
        for(var 
0userRequests[id].lengthi++)
            
userRequests[id](erruser);

        
delete userRequests[id];
    };

    
// try load user from db
    
db.collection('users').findOne({
        
_idid
    
}, {
        
fields: {
            
name1,
            
friend_ids1
        
}
    }, function(
erritem) {
        var 
user null;

        
// if user loaded, create cache
        
if (! err && item)
            
user users[id] = new User(item);

        
finish(erruser);
    });
};

// add client to user
var addClient = function(userIdsocket) {
    
userGetOrLoad(userId, function(erruser) {
        if (
err || ! user)
            return;

        var 
added user.socketAdd(socket);

        if (
added) {
            
// notify all friends about user being online
            
for(var 0user.friends.lengthi++) {
                var 
friend users[user.friends[i]];
                if (! 
friend) continue;
                
friend.send('friend:online', { iduser.id });
            }
        }
    });
};

// remove client from user
var removeClient = function(userIdsocket) {
    var 
user users[userId];

    if (! 
user)
        return;

    
user.socketRemove(socket);

    if (
user.connections === 0) {
        
delete users[userId];

        
// notify all friends that user is offline now
        
for(var 0user.friends.lengthi++) {
            var 
friend users[user.friends[i]];
            if (! 
friend) continue;
            
friend.send('friend:offline', { iduser.id });
        }
    }
}; 


St_AnGer 02.11.2015 20:20

Ответ: Чат: PHP + MySQLi или что то другое?
 
Хм, неожиданный косяк выскочил у меня тут при попытке сортировки по полю формата ISODate. При сортировке по таковому полю происходит следующее:
по ВРЕМЕНИ сортируется ровно как я задаю, а по ДАТЕ сортируется прямо противоположно.
ЧЯДНТ?

То есть имея несколько документов
Код:

{...,date:ISODate("2015-11-01T12:58:53.724Z"),...}
{...,date:ISODate("2015-11-02T11:58:53.724Z"),...}
{...,date:ISODate("2015-11-01T14:58:53.724Z"),...}
{...,date:ISODate("2015-11-02T12:58:53.724Z"),...}
{...,date:ISODate("2015-11-02T10:58:53.724Z"),...}

и совершая выборку с сортировкой по возрастанию
Код:

db.collection.find(...).sort({date:1})
результат имеем следующий:
Код:

{...,date:ISODate("2015-11-02T10:58:53.724Z"),...}
{...,date:ISODate("2015-11-02T11:58:53.724Z"),...}
{...,date:ISODate("2015-11-02T12:58:53.724Z"),...}
{...,date:ISODate("2015-11-01T12:58:53.724Z"),...}
{...,date:ISODate("2015-11-01T14:58:53.724Z"),...}

Как же так? Мне казалось что сортировать должно и по дате, и по времени в том формате, в котором я задал (т.е. или ASC или DESC, а не пополам)...


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


Часовой пояс GMT +4, время: 02:25.

vBulletin® Version 3.6.5.
Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
Перевод: zCarot