Всем привет! Когда речь заходит о разработке высоконагруженных систем, многие предлагают: «python, сделать проще и быстрее». Но есть проблема: Скорость разработки!= скорость и качество работы сервиса. Когда мы делаем любой продукт, важны: Масштабируемость, стабильность работы под большой нагрузкой, предсказуемость поведения системы — особенно когда речь идет о тысячах одновременных пользователей, лентах новостей в реальном времени, уведомлениях и сложных связях между разными сущностями.
Именно поэтому и выбрана Java и Spring Boot как основа. Банки и видеосервисы работают именно на Java и Spring.
Одна из особенностей: строгая типизация, качественные инструменты для многопоточности, мощная экосистема и предсказуемое потребление памяти делают Java идеальным выбором для систем, где падение = полная потеря пользователей.
Планирую написать цикл статей по разработке соцсети, которая будет объединять в себе ВК, пикабу, и иже с ними. Не ради «создания продукта который затмит всех и вся» а ради самого программирования.
Итак, начну, среда разработки Intellij Idea. Кстати, весь проект будет доступен из gitLab. Нам необходимо создать файлы с конфигурацией. можно сказать что их обычно 3. Да, я про application.yml. один из них общий, который так и называется: application.yml, и еще два: application-dev.yml и application-prod.yml
spring: main: allow-bean-definition-overriding: false servlet: multipart: max-file-size: 200MB max-request-size: 200MB recaptcha: secret: ${RECAPTCHA_SECRET_SOCIAL} verify-url: ${VERIFY_URL} flyway: enabled: true locations: classpath:db/migration baseline-on-migrate: true datasource: url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME_ROOT_CORE_SOCIAL} username: ${SQL_LOGIN_ROOT_CORE_SOCIAL} password: ${SQL_PASSWORD_ROOT_CORE_SOCIAL} driver-class-name: org.postgresql.Driver mail: host: smtp.mail.ru port: 587 username: ${MAIL_SOCIAL} password: ${MAIL_PASSWORD_SOCIAL} properties: mail: smtp: auth: true starttls: enable: true jpa: properties: hibernate: default_schema: root_core_social database-platform: org.hibernate.dialect.PostgreSQLDialect logging: level: org.springframework.security: INFO jwt: secret: ${SECRET_KEY_ROOT_CORE_SOCIAL} access-expiration: 900000 refresh-expiration: 2592000000
Как можно заметить, все критически важные данные указаны как переменные окружения. Это критически важно для безопасности.
Два других файла определяют некоторые параметры, в частности, в dev версии мы в логи будем писать sql запросы в БД, чтобы видеть что у нас происходит на сервере.
Далее нам необходимо создать схему БД, и для этого мы воспользуемся миграцией, миграции БД нам осуществляет flyway. И первая "миграция" это определение схемы БД.
create schema IF NOT EXISTS root_core_social;
Первое что я делаю, это создание структуры БД, нам необходимо создать первую таблицу, которая будет определять пользователя.
Многие знают, что необходимо указывать определенные поля в таблице. Обязательный пункт: id пользователя. Во многих таблицах, в учебниках, указывают id тип Long (BIGINT), Это большая цифра, в ней «поместятся» все пользователи по количеству, но это не самый лучший вариант, и объяснение тянет на отдельную статью. Причем, такой элемент будет находиться в каждой таблице, то есть это общий элемент. а значит создавать в каждой таблице параметр нет необходимости. И, кроме него, также необходимо создать еще два обязательных пункта: время создания и время изменения.
@MappedSuperclass @Getter @Setter public abstract class AbstractEntityUuid { @Id @GeneratedValue(strategy = GenerationType.UUID) @Column(name = "id", nullable = false, updatable = false) private UUID uuid; @Override public boolean equals(Object o){ if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AbstractEntityUuid that = (AbstractEntityUuid) o; return uuid != null && uuid.equals(that.uuid); } @Override public int hashCode(){ return Objects.hash(uuid); } }
Нам необходимо создать айдишник, который будет использоваться во всех таблицах, а так-же два метода, equals определяет что это один и тот же объект в смысле бизнес логики, а hashCode обеспечивает быстрый поиск в хеш-коллекциях.
Следующий пункт, нам нужно два поля в таблице, это время создания и время изменения.
@MappedSuperclass @Getter @Setter @RequiredArgsConstructor @AllArgsConstructor public abstract class DatedEntity extends AbstractEntityUuid { @CreationTimestamp @Column(name = "created_at") private LocalDateTime createdAt; @UpdateTimestamp @Column(name = "updated_at") private LocalDateTime updatedAt; }
Это у нас абстрактный базовый класс для сущностей с автоматически управляемыми временными метками: createdAt - время создания, и updatedAt время последнего обновления. Класс наследует предыдущий класс, и, следовательно, наследует id. Класс используется для единоразового аудита изменений всех сущностей в системе. Заполняет их Hebernate.
Итак, база готова, теперь при создании таблиц мы будем просто наследоваться от класса DatedEntity и у нас будут подтягиваться поля БД от них.
Создаем теперь пользователя:
@Entity @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Table(name = "user_root_core") public class UserEntity extends DatedEntity { @Column(name = "mail", nullable = false, unique = true) private String mail; }
Наш пользователь наследовался от DatedEntity и получил 3 поля из суперклассов. Есть такое правило: Все что может быть отдельным объектом, должно быть отдельным объектом, НО! тут главное не переуседствовать, иначе у нас все будет работать очень и очень медленно! Поэтому таблица UserEntity имеет так мало полей. Захотим создать строки с "паспортом" пользователя? мы не будем делать отдельно миграцию, которая будет у нас в эту таблицу прописывать дополнительные поля. мы просто сделаем еще одну таблицу.
Следующие два класса будут наследоваться так же от DatedEntity, и они будут в себе содержать по одному полю: private UserEntity userUuid; но с разными типами связей.
@OneToOne и @ManyToOne, думаю, смысл понятен: все классы, у которых родитель UserEntity в зависимости от типа связи будет подключаться нужный из них и таким образом мы избежим дублирования кода.
@MappedSuperclass @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class UserOwnerOneToOne extends DatedEntity { @OneToOne @JoinColumn(name = "user_uuid", nullable = false) private UserEntity userUuid; }
@MappedSuperclass @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class UserOwnerManyToOne extends DatedEntity { @ManyToOne @JoinColumn(name = "user_uuid", nullable = false) private UserEntity userUuid; }
Итак, у нас созданы все базовые классы для таблиц в БД. Теперь нам необходимо создать остальные таблицы. Следующая нужная нам таблица - информация из яндекса. Да, будет реализована регистрация пользователей именно через яндекс, потому как смс это вещь, которая стоит денег и весьма не маленьких. Регистрация просто через почту создает угрозу появления ботов в огромных количествах, а регистрация через смс имеет свойство нивели��овать эту проблему, не полностью, но все же.
При регистрации через яндекс мы будем получать от яндекса некоторые параметры, и класть их в нашу таблицу. Мы будем получать:
Почта
Номер телефона
Пол
Имя
Фамилию
Яндекс ID
Полученные данные мы поместим в таблицу:
@Entity @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Table(name = "yandex_user_info") public class YandexUserInfoEntity extends UserOwnerOneToOne { @Column(name = "yandex_id", nullable = false, unique = true) private String yandexId; @Column(name = "first_name") private String firstName; @Column(name = "last_name") private String lastName; @Column(name = "gender") private String gender; @Column(name = "email") private String email; @Column(name = "phone") private String phone; }
Стоит заметить что никакие параметры, кроме "name" мы не ставим. По умолчанию без дополнительных параметров у нас будет тип VARCHAR(255) и, как можно понять, будет 255 символов, нам этого более чем достаточно. Еще можно заметить что значения могут быть null, потому что мы не указали обратного, пол пользователя или имя, например, могут быть не указаны.
следом идем к профилю пользователя. Можно сказать что профиль, это «публичная» таблица, в ней собраны самые необходимые параметры, которые мы не «боимся раскрыть».
@Entity @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Table(name = "user_profile") public class UserProfileEntity extends UserOwnerOneToOne { @Column(name = "about_user", length = 1000) private String aboutUser; @Column(name = "nickname", unique = true, nullable = false) private String nickName; @Column(name = "status", nullable = false) @Enumerated(EnumType.STRING) private StatusUserEnum status; @Column(name = "public_id", unique = true, nullable = false) private UUID publicId; @Column(name = "role", nullable = false) @Enumerated(EnumType.STRING) private RoleUserEnum role; @Column(name = "avatar_icon") private String avatarIcon; }
Тут можно указать что статус роль у нас вынесены отдельно, в enum. Нужно, естественно, чтобы не ошибиться при назначении:
@Getter public enum StatusUserEnum { ACTIVE("active"), BLOCKED("blocked"), PENDING("pending"), SUSPENDED("suspended"), DEACTIVATED("deactivated"); private final String code; StatusUserEnum(String code) { this.code = code; } public static StatusUserEnum fromCode(String code) { for (StatusUserEnum r : values()) { if (r.code.equals(code)) return r; } throw new IllegalArgumentException("Unknown role: " + code); } }
@Getter public enum RoleUserEnum { USER("user"), MODERATOR("moderator"), ADMIN("admin"), SUPER_ADMIN("super_admin"); private final String code; RoleUserEnum(String code) { this.code = code; } public static RoleUserEnum fromCode(String code) { for (RoleUserEnum r : values()) { if (r.code.equals(code)) return r; } throw new IllegalArgumentException("Unknown role: " + code); } }
С одной стороны, возникает вопрос: а почему бы все эти параметры не поместить в таблицу UserEntity? Все очень просто, в этой таблице нет никаких личных персональных данных, она состоит из данных пользователя, которые имеют открытый доступ и будут представлены на сайте. В то время как UserEntity хранит личные персональные данные, в частности почту.
Еще одна важная таблица:
@Entity @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Table(name = "nickname_old") public class NicknameOldEntity extends UserProfileOwnerManyToOne { @Column(name = "nickname") private String nickName; }
Ранее мы уже создавали суперклассы для UserEntity, но, при работе с будущим приложением с UserEntity мы взаимодействовать практически не будем, но дополнения еще будем вносить. Все взаимодействие мы обеспечим с таблицей UserProfile. А, значит, нам необходимо создать суперкласс и для него.
@MappedSuperclass @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class UserProfileOwnerManyToOne extends DatedEntity { @ManyToOne @JoinColumn(name = "user_uuid", nullable = false) private UserProfileEntity userUuid; }
При создании таблицы постов мы будем обращаться именно к ней.
Дело в том, что при работе с любым приложением, чем меньше мы обращаемся к базе, тем быстрее приложение работает.
В нашей структуре реализован такой принцип:
Чтобы получить данные пользователя мы обращаемся к таблице UserProfile и получаем все данные, которые нам необходимы по пользователю. Когда будут реализованы посты пользователей, то основное взаимодействие в ленте будет с таблицей, которая будет хранить в себе посты.
Таким образом мы получаем минимальное обращение к базе данных, и, следовательно, максимальную скорость работы приложения. Те параметры что нам не нужны или закрыты пользователь не получает, а, значит, мы не боимся случайно отправить на фронт те данные, которые носят личный характер.
ссылка на gitLab
Всем спасибо за внимание! Это мой первый пост, не будьте слишком строги. Мог что то забыть или упустить.
