MongoDB — структурирование и запросы моделей и схем для отношений «многие ко многим»

Пытаясь бороться с недостатками отношений в MongoDB, вот что я пытаюсь иметь в своей базе данных:

Модели:

  1. Пользователи: (уже есть много пользователей в базе данных) в основном запрашиваются через их uid, но _id также доступен.
  2. Команды: содержит 1 владельца, несколько участников, проекты и активы.
  3. Участники: могут принадлежать к нескольким командам, каждый член может иметь разные роли. Участник может принадлежать к нескольким командам с разными ролями.
  4. Роли: принадлежит участникам. Участник может иметь разные роли в каждой команде, к которой он принадлежит. ['просмотр', 'редактирование', 'админ']. Роли также могут быть встроены в модель участников, чтобы использовать гибридный метод.
  5. Проекты: проекты могут быть независимыми или частью команды, доступ к проектам, принадлежащим командам, определяется ролью участника.
  6. Активы: Активы могут быть независимыми или частью команды, доступ к активам, принадлежащим командам, определяется ролью члена. Актив также может содержать несколько проектов.

Характеристики:

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

Член команды может иметь доступ к активам и проектам, к которым владелец группы решил, что у них есть доступ.

Участник может создавать свои собственные частные активы и проекты.

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

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

Участник может просматривать список ресурсов и проектов, которыми с ним поделились в команде.

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

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

Текущая схема:

const teamSchema = new Schema({
  uid: {
    type: String,
    required: true
  },
  name: {
    type: String,
    required: true
  },
  members: {
    type: [String]
  },
  brands: {
    type: [String]
  }
}, { collection: 'team', timestamps: true });

const memberSchema = new Schema({
  uid: {
    type: String,
    required: true
  },
  owner: {
    type: String,
    required: true
  },
  teamID: {
    type: Schema.Types.ObjectId,
    required: true
  },
  acl: {
    type: String,
    required: true,
    enum: ['view', 'copy', 'edit', 'admin', 'owner'],
    default: 'view'
  },
  pending: {
    type: Boolean,
    default: true
  }
}, { collection: 'member', timestamps: true, _id: false });

const assetsSchema = new Schema({
  uid: {
    type: String,
    required: true
  },
  title: {
    type: String,
    required: true
  },
  fonts: [AssetFile],
  colors: [Color],
  logos: [AssetFile],
  images: [AssetFile],
  projects: Array,
  items: Array,
  members: [String]
}, { collection: 'branding', timestamps: true });

const userSchema = new Schema({
  uid: {
    type: String,
    required: true
  },
  email: {
    type: String,
    required: true,
    lowercase: true,
    index: { unique: true },
    validate: v => validator.isEmail(v)
  },
  avatar: String,
  files: [FilesSchema],
  isAdmin: Boolean,
  projectCount: {
    type: Number,
    default: 0
  },
  userName: {
    type: String,
    trim: true
  },
  userType: {
    type: String,
    required: true,
    default: 'Free User'
  },
  lastSeenAt: {
    type: Date
  },
}, { collection: 'user', timestamps: true });

const projectSchema = new Schema({
  uid: {
    type: String,
    required: true
  },
  objects: Array,
  projectTitle: {
    type: String,
    trim: true,
    default: 'Untitled Project'
  },
  preview: {
    type: String
  },
  folder: {
    type: String,
    trim: true,
    default: null
  },
  archived: {
    type: Boolean,
    default: false
  },
}, { collection: 'project', timestamps: true });

Этого можно легко достичь в реляционных базах данных, но, поскольку мое приложение уже находится в открытом доступе с более чем 40 000 пользователей, переход на другую БД — непростая задача.

Можно ли этого достичь в MongoDB и как, или мне следует прекратить попытки и перейти на другую БД?

Я уже использую mongoose, но я также открыт для использования собственных кодов MongoDB (таких как агрегат, $lookup и т. д.)

Обновлено:

Для меня это больше вопрос масштабируемости, чем обучение написанию запросов в MongoDB, поэтому вот запросы, которые у меня уже есть:

Список активов:

const personalAssets = await AssetsModel.aggregate([
    { $match: { uid: user.uid } },
    { $project: { _id: 1, uid: 1, title: 1, logos: 1, teamID: 1, acl: 'owner', createdAt: 1, fonts: 1 } },
    { $sort: { createdAt: -1 } }
]);

const sharedAssets = await MemberModel.aggregate([
    {
    $match: {
        $or: [
        { uid: user.uid },
        { owner: user._id }
        ]
    }
    },
    { $project: { _id: 0, acl: 1, teamID: 1, owner: 1 } },
    {
    $lookup: {
        from: 'team',
        let: { team_id: '$teamID' },
        pipeline: [
        {
            $match: {
            $expr: {
                $eq: [ '$_id', '$$team_id' ]
            }
            }
        },
        { $project: { _id: 0, assets: 1, teamID: '$$team_id' } }
        ],
        as: 'team'
    }
    },
    { $replaceRoot: { newRoot: { $mergeObjects: [ { $arrayElemAt: [ '$team', 0 ] }, '$$ROOT' ] } } },
    { $project: { assets: 1, acl: 1, teamID: 1, owner: 1 } },
    { $match: { 'assets.0': { $exists: true } } },
    {
    $lookup: {
        from: 'branding',
        let: { 'assets': '$assets' },
        pipeline: [
        {
            $match: {
            $expr: {
                $and: [
                { $in: [ '$_id', '$$assets' ] },
                { $ne: [ '$uid', user.uid ] }
                ]
            }
            }
        }
        ],
        as: 'assets'
    }
    },
    { $unwind: '$assets' },
    {
    $replaceRoot: {
        newRoot: {
        $mergeObjects: ['$assets', { acl: '$acl' }, { teamID: '$teamID' }, { owner: '$owner' }]
        }
    }
    },
    {
    $project: {
        assets: 1,
        acl: {
        $cond: {
            if: { $eq: [ user._id, '$owner' ] },
            then: 'owner',
            else: '$acl'
        }
        },
        _id: 1,
        uid: 1,
        title: 1,
        teamID: 1,
        logos: 1,
        fonts: 1
    }
    }
]);

return [...personalAssets, ...sharedAssets];

Отдельные активы:

const personalAsset = await AssetsModel.findOne({ _id, uid: user.uid });
if (brand) asset.acl = 'owner';

const sharedAsset = await MemberModel.aggregate([
    {
        $match: {
        $and: [
            { teamID: mongoose.Types.ObjectId(teamID) },
            {
            $or: [
                { uid: user.uid },
                { owner: user._id }
            ]
            }
        ]
        }
    },
    { $project: { _id: 0, acl: 1, teamID: 1, owner: 1 } },
    {
        $lookup: {
        from: 'team',
        pipeline: [
            {
            $match: {
                $expr: {
                $and: [
                    { $eq: [ '$_id', mongoose.Types.ObjectId(teamID) ] },
                    { $in: [ mongoose.Types.ObjectId(_id), '$assets' ] }
                ]
                }
            }
            },
            { $project: { _id: 0, assets: 1, teamID: teamID } }
        ],
        as: 'team'
        }
    },
    { $replaceRoot: { newRoot: { $mergeObjects: [ { $arrayElemAt: [ '$team', 0 ] }, '$$ROOT' ] } } },
    { $match: { 'assets.0': { $exists: true } } },
    {
        $lookup: {
        from: 'branding',
        pipeline: [
            {
            $match: {
                $expr: {
                $eq: [ '$_id', mongoose.Types.ObjectId(_id) ]
                }
            }
            }
        ],
        as: 'assets'
        }
    },
    { $unwind: '$assets' },
    {
        $replaceRoot: {
        newRoot: {
            $mergeObjects: [
            '$assets',
            {
                acl: {
                $cond: {
                    if: { $eq: [ user._id, '$owner' ] },
                    then: 'owner',
                    else: '$acl'
                }
                }
            },
            { teamID: '$teamID' }
            ]
        }
        }
    }
]);

brand = sharedAsset[0];

То же самое касается папок и проектов. Основная проблема здесь заключается в получении списка активов и получении членского ACL (уровня управления доступом). Это проще сделать для отдельных ресурсов (папок и проектов).

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

Я использовал $lookup для объединения нескольких коллекций. Если вы выбираете базу данных nosql, такую ​​​​как mongo, ваша текущая схема imo не самая лучшая. Во-первых, немного сложно выразить свои запросы с помощью агрегата. Во-вторых, его не так просто поддерживать по мере роста ваших требований. Я бы посоветовал перейти на rdbms, прежде чем он станет слишком пустым или денормализует вашу текущую схему. Монго никогда не предназначался для того, чего вы пытаетесь достичь.

HIRA THAKUR 09.02.2019 13:10

Она еще не опубликована, поэтому у меня есть возможность переписать схему для всего вышеперечисленного, кроме схемы пользователя. Как вы предлагаете денормализовать схему, а затем запрашивать/агрегировать в соответствии с тем, что я объяснил выше?

Hooman Askari 09.02.2019 13:13

Не могли бы вы поделиться, какая у вас реальная проблема. Какой-то конкретный запрос/агрегация, которого вы не смогли достичь? Если это производительность, что является узким местом? и Т. Д.

Wan Bachtiar 13.02.2019 04:05

@WanBachtiar Я только что отредактировал свой вопрос и добавил запрос, который использую для схемы активов. У меня все запросы работают, меня больше всего беспокоит его масштабируемость в будущем. Например, в ближайшие месяцы будет добавлена ​​совместная работа в режиме реального времени, и я хочу, чтобы мои данные были структурированы таким образом, чтобы их было легче расширять.

Hooman Askari 13.02.2019 09:03

Хотя агрегации должны хорошо работать с индексированными ключами в $match, вы также можете рассмотреть возможность параллельного запуска нескольких запросов на чтение и объединения их в своем приложении. (Если у вас есть отношения на основе ссылок в корневой модели). Время поиска Mongo для индексированных ключей очень быстрое. Nodejs делает асинхронное параллельное выполнение второй натурой для вас (он был разработан для такого типа вещей). Время отклика не будет таким медленным, как вы могли бы ожидать по привычке. Примечание: ваша схема может быть упрощена для более подходящего моделирования NoSQL.

Shasak 15.02.2019 20:24

@Shasak Я уже использую параллельные асинхронные запросы, которые отлично работают, когда я извлекаю отдельные документы. Сложная часть — выборка массивов, а затем сопоставление списка контроля доступа с результатами актива или члена. Агрегированные запросы выполняют свою работу. Я просто надеюсь, что они тоже масштабируемы. Для вашего примечания, как вы предлагаете денормализировать его, чтобы упростить моделирование и сделать его более подходящим для NoSQL?

Hooman Askari 16.02.2019 21:16
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
6
805
0

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