学习

使用 React、NodeJS 和 Redis 构建电影数据库应用程序

Ajeet Raina
作者
Ajeet Raina, Redis 前开发人员增长经理
生命周期结束通知

Redis 正在逐步淘汰 RedisGraph此博客文章 解释了此决定的动机以及对现有 Redis 客户和社区成员的影响。

支持结束时间定于 2025 年 1 月 31 日。

IMDb(互联网电影数据库)是世界上最受欢迎和最权威的电影、电视剧和名人信息来源。此应用程序是 IMDb 克隆,具有基本帐户身份验证和电影推荐功能。您将学习 RedisGraph 和 NodeJS 的强大功能来构建简单的电影数据库。

技术栈#

  • 前端 - React
  • 后端 - Node.js、Redis、RedisGraph

步骤 1. 安装先决条件#

  • Node - v13.14.0+
  • NPM - v7.6.0+

步骤 2. 运行 Redis Stack Docker 容器#

 docker run -d -p 6379:6379 redis/redis-stack

确保 Docker 容器已启动并正在运行

 docker ps
 CONTAINER ID   IMAGE                           COMMAND                  CREATED             STATUS             PORTS                    NAMES
 fd5ef30f025a   redis/redis-stack               "redis-server --load…"   2 hours ago         Up 2 hours         0.0.0.0:6379->6379/tcp   nervous_buck

步骤 3. 运行 RedisInsight Docker 容器#

 docker run -d -v redisinsight:/db -p 8001:8001 redislabs/redisinsight:latest

确保 Docker 容器已启动并正在运行

 docker ps
 CONTAINER ID   IMAGE                           COMMAND                  CREATED             STATUS             PORTS                    NAMES
 264db1706dcc   redislabs/redisinsight:latest   "bash ./docker-entry…"   About an hour ago   Up About an hour   0.0.0.0:8001->8001/tcp   angry_shirley
 fd5ef30f025a   redis/redis-stack               "redis-server --load…"   2 hours ago         Up 2 hours         0.0.0.0:6379->6379/tcp   nervous_buck

步骤 4. 克隆存储库#

 git clone https://github.com/redis-developer/basic-redisgraph-movie-demo-app-nodejs

步骤 5. 设置环境变量#

复制 .env.sample 到 .env 并添加以下详细信息:

  REDIS_ENDPOINT_URL = "Redis server URI"
  REDIS_PASSWORD = "Password to the server"

步骤 6. 安装依赖项#

 npm install

步骤 7. 运行后端服务器#

 node app.js

步骤 8. 运行客户端#

 cd client
 yarn install
 yarn start

步骤 9. 访问电影应用程序#

打开 https://IP:3000 访问电影应用程序

步骤 10. 注册新帐户#

输入详细信息以创建新帐户

步骤 11. 登录电影应用程序#

步骤 12. 评分电影#

步骤 13. 查看评分电影列表#

步骤 14. 通过 RedisInsight 查看导演的电影#

 GRAPH.QUERY "MovieApp" "MATCH (director:Director {tmdbId: \"4945\"})-[:DIRECTED]->(movie:Movie) RETURN DISTINCT movie,director"

步骤 15. 查找演员参演的电影。#

在 RedisGraph 中运行以下查询以查找作者参演的电影

 GRAPH.QUERY "MovieApp" "MATCH (actor:Actor {tmdbId: \"8537\"})-[:ACTED_IN_MOVIE]->(movie:Movie) RETURN DISTINCT movie,actor"

步骤 16. 将用户存储在数据库中#

 CREATE (user:User {id: 32,
 username: "user", password: "hashed_password", api_key: "525d40da10be8ec75480"})
 RETURN user

步骤 17. 通过用户名查找用户#

 MATCH (user:User {username: "user"}) RETURN user

它是如何工作的?#

该应用程序使用 Express API 提供的数据并通过一些视图将其呈现给最终用户,包括

  • 主页
  • 注册和登录页面
  • 电影详情页面
  • 演员和导演详情页面
  • 用户详情页面

主页#

主页显示流派以及与其相关的电影简要列表。

数据存储方式#

添加新流派:#

 create (g:Genre{name:"Adventure"})

添加电影:#

 create (m:Movie {
  url: "https://themoviedb.org/movie/862",
  id:232,
  languages:["English"],
  title:"Toy Story",
  countries:["USA"],
  budget:30000000,
  duration:81,
  imdbId:"0114709",
  imdbRating:8.3,
  imdbVotes:591836,
  movieId:42,
  plot:"...",
  poster:"https://image.tmd...",
  poster_image:"https://image.tmdb.or...",
  released:"1995-11-22",
  revenue:373554033,
  runtime:$runtime,
  tagline:"A cowboy doll is profoundly t...",
  tmdbId:"8844",
  year:"1995"})

将流派设置为电影:#

 MATCH (g:Genre), (m:Movie)
 WHERE g.name = "Adventure" AND m.title = "Toy Story"
 CREATE (m)-[:IN_GENRE]->(g)

数据访问方式#

获取流派:#

 MATCH (genre:Genre) RETURN genre

按流派获取电影:#

 MATCH (movie:Movie)-[:IN_GENRE]->(genre)
 WHERE toLower(genre.name) = toLower("Film-Noir") OR id(genre) = toInteger("Film-Noir")
 RETURN movie

代码示例:获取流派为的电影#

 const getByGenre = function (session, genreId) {
 const query = [
  'MATCH (movie:Movie)-[:IN_GENRE]->(genre)',
  'WHERE toLower(genre.name) = toLower($genreId) OR id(genre) = toInteger($genreId)',
  'RETURN movie',
 ].join('\n');

 return session
 .query(query, {
    genreId,
  })
  .then((result) => manyMovies(result));
};

注册和登录页面#

为了能够对电影进行评分,用户需要登录:为此,实现了基于 JWT 的基本身份验证系统,其中用户详细信息存储在 RedisGraph 中以实现持久性。

数据存储方式#

将用户存储在数据库中:#

 CREATE (user:User {id: 32,
 username: "user", password: "hashed_password", api_key: "525d40da10be8ec75480"})
 RETURN user

数据访问方式#

通过用户名查找:#

 MATCH (user:User {username: "user"}) RETURN user

代码示例:查找用户#

 const me = function (session, apiKey) {
 return session
   .query('MATCH (user:User {api_key: $api_key}) RETURN user', {
     api_key: apiKey,
   })
   .then((foundedUser) => {
     if (!foundedUser.hasNext()) {
       throw {message: 'invalid authorization key', status: 401};
     }
     while (foundedUser.hasNext()) {
       const record = foundedUser.next();
       return new User(record.get('user'));
     }
   });
 };

电影详情页面#

在此页面上,用户可以对电影进行评分,并查看参与电影制作的演员/导演。

数据存储方式#

将演员与电影关联:#

 MATCH (m:Movie) WHERE m.title="Jumanji" CREATE (a:Actor :Person{
 bio:"Sample...",
 bornIn:"Denver, Colorado, USA",
 imdbId:"0000245",
 name:"Robin Williams",
 poster:"https://image.tmdb.org/t/p/w440_and_...",
 tmdbId:"2157",
 url:"https://themoviedb.org/person/2157"})-[r:ACTED_IN_MOVIE
 {role: "Alan Parrish"}]->(m)

将导演与电影关联:#

 MATCH (m:Movie) WHERE m.title="Dead Presidents" CREATE (d:Director :Person{
  bio: "From Wikipedia, the free e...",
  bornIn: "Detroit, Michigan, USA",
  imdbId: "0400436",
  name: "Albert Hughes",
  tmdbId: "11447",
  url: "https://themoviedb.org/person/11447"})-[r:DIRECTED]->(m)

数据访问方式#

通过 ID 查找电影,包括流派、演员和导演:#

 MATCH (movie:Movie {tmdbId: $movieId})
 OPTIONAL MATCH (movie)<-[my_rated:RATED]-(me:User {id: "e1e3991f-fe81-439e-a507-aa0647bc0b88"})
 OPTIONAL MATCH (movie)<-[r:ACTED_IN_MOVIE]-(a:Actor)
 OPTIONAL MATCH (movie)-[:IN_GENRE]->(genre:Genre)
 OPTIONAL MATCH (movie)<-[:DIRECTED]-(d:Director)
 WITH DISTINCT movie, my_rated, genre, d,  a, r
 RETURN DISTINCT movie,
 collect(DISTINCT d) AS directors,
 collect(DISTINCT a) AS actors,
 collect(DISTINCT genre) AS genres

代码示例:获取电影详情#

 const getById = function (session, movieId, userId) {
 if (!userId) throw {message: 'invalid authorization key', status: 401};
 const query = [
   'MATCH (movie:Movie {tmdbId: $movieId})\n' +
     '  OPTIONAL MATCH (movie)<-[my_rated:RATED]-(me:User {id: $userId})\n' +
     '  OPTIONAL MATCH (movie)<-[r:ACTED_IN_MOVIE]-(a:Actor)\n' +
     '  OPTIONAL MATCH (movie)-[:IN_GENRE]->(genre:Genre)\n' +
     '  OPTIONAL MATCH (movie)<-[:DIRECTED]-(d:Director)\n' +
     '  WITH DISTINCT movie, my_rated, genre, d,  a, r\n' +
     '  RETURN DISTINCT movie,\n' +
     '  collect(DISTINCT d) AS directors,\n' +
     '  collect(DISTINCT a) AS actors,\n' +
     '  collect(DISTINCT genre) AS genres',
 ].join(' ');
 return session
   .query(query, {
     movieId: movieId.toString(),
     userId: userId.toString(),
   })
   .then((result) => {
     if (result.hasNext()) {
       return _singleMovieWithDetails(result.next());
     }
     throw {message: 'movie not found', status: 404};
   });
 };

演员和导演详情页面#

数据访问方式#

查找演员参演的电影:#

 MATCH (actor:Actor {tmdbId: "8537"})-[:ACTED_IN_MOVIE]->(movie:Movie)
 RETURN DISTINCT movie,actor

查找导演的电影:#

 MATCH (director:Director {tmdbId: "4945"})-[:DIRECTED]->(movie:Movie)
 RETURN DISTINCT movie,director

获取导演的电影#

 const getByDirector = function (session, personId) {
 const query = [
   'MATCH (director:Director {tmdbId: $personId})-[:DIRECTED]->(movie:Movie)',
   'RETURN DISTINCT movie,director',
 ].join('\n');

 return session
   .query(query, {
     personId,
   })
   .then((result) => manyMovies(result));
 };

用户详情页面#

显示用户资料信息和评分的电影#

数据存储方式#

设置电影评分:#

 MATCH (u:User {id: 42}),(m:Movie {tmdbId: 231})
 MERGE (u)-[r:RATED]->(m)
 SET r.rating = "7"
 RETURN m

数据访问方式#

获取电影和用户评分:#

 MATCH (:User {id: "d6b31131-f203-4d5e-b1ff-d13ebc06934d"})-[rated:RATED]->(movie:Movie)
 RETURN DISTINCT movie, rated.rating as my_rating

获取用户的评分电影#

 const getRatedByUser = function (session, userId) {
 return session
   .query(
     'MATCH (:User {id: $userId})-[rated:RATED]->(movie:Movie) \
      RETURN DISTINCT movie, rated.rating as my_rating',
     {userId},
   )
   .then((result) =>
     result._results.map((r) => new Movie(r.get('movie'), r.get('my_rating'))),
   );
 };

数据类型:#

  • 数据存储在各种键和各种关系中。
  • 有 5 种类型的数据
  • 用户
  • 导演
  • 演员
  • 流派
  • 电影

每种类型都有自己的属性#

  • 演员:id, bio, born , bornIn, imdbId, name, poster, tmdbId, url
  • 类型: id, name
  • 导演: id, born, bornIn, imdbId, name, tmdbId, url
  • 用户: id, username, password, api_key
  • 电影: id, url, languages, countries, budget, duration, imdbId, imdbRating, indbVotes, movieId, plot, poster, poster_image, released, revenue, runtime, tagline, tmdbId, year

还有 4 种关系类型:#

  • 用户-RATED->电影
  • 导演-DIRECTED->电影
  • 演员-ACTED_IN_MOVIE->电影
  • 电影-IN_GENRE->类型

参考资料#