Gem ActionStore - 一步到位的 Like, Follow, Star, Block ... 等动作的解决方案

huacnlee for Rails Engine Gem · 发布于 2017年2月08日 · 最后由 hegwin 回复于 2017年3月18日 · 3398 次阅读
2
本帖已被设为精华帖!

各类系统总会或多或少的需要一些赞、收藏、关注/订阅等功能。

每次我们都要重新开发么?不!那不是 DRY 的风格。

鉴于多次这类功能的实践总结,我发布了 ActionStore,一个可以帮你解决这类需求的库。

介绍

ActionStore 采用 Active Record 多态关联(Polymorphic Association)的方式存储各种类型的动作数据,例如:赞、喜欢、收藏、关注、订阅、屏蔽(靠你的想象,还可以干更多的事情)等等,各类 User -> Target 的场景。

因此你无需对这些动作重复重建表、Model,也不用重复写 like_topic, unlike_topic, @topic.like_by_users, @user.like_topics 之类的逻辑函数。一切都由 ActionStore 内部帮你处理掉。

项目地址:

https://github.com/rails-engine/action-store

适用的场景

  • 赞(Like) - Post / Topic / Reply / Comment / Issue ...
  • 订阅 (Subscribe) - Post / Category
  • 好友关注 (Follow) - User
  • 收藏 (Star) - Post / Topic / Reply

基本结构

Field 含义
action_type action 类型 [like, watch, follow, star, favorite]
user_type, user_id 多态的方式关联 User models [User, Person, Member]
target_type, target_id 多态的方式关联 Target models [User, Post, Comment]

使用方法

gem 'action-store'

and run bundle install

$ rails g action_store:install
create  config/initializers/action_store.rb
migration 20170208024704_create_actions.rb from action_store

将会把 Migration 放入你的项目里面,最后执行 rails db:migrate

声明 Actions

你需要在 User model 里面使用 action_store 来定义 actions:

app/models/user.rb

class User < ActiveRecord::Base
  # action_store <action_type>, <target>, opts

  # 例如
  action_store :like, :post, counter_cache: true
  action_store :star, :post, counter_cache: true, user_counter_cache: true
  action_store :follow, :post
  action_store :like, :comment, counter_cache: true
  action_store :follow, :user, counter_cache: 'followers_count', user_counter_cache: 'following_count'
end

Convention Over Configuration:

action, target Target Model Target counter_cache_field User counter_cache_field Target has_many User has_many
action_store :like, :post Post has_many :like_by_user_actions, has_many :like_by_users has_many :like_post_actions, has_many :like_posts
action_store :like, :post, counter_cache: true Post likes_count has_many :like_by_user_actions, has_many :like_by_users has_many :like_post_actions, has_many :like_posts
action_store :star, :project, class_name: 'Repository' Repository stars_count star_projects_count has_many :star_by_user_actions, has_many :star_by_users
action_store :follow, :user User follows_count follow_users_count has_many :follow_by_user_actions, has_many :follow_by_users has_many :follow_user_actions, has_many :follow_users
action_store :follow, :user, counter_cache: 'followers_count', user_counter_cache: 'following_count' User followers_count following_count has_many :follow_by_user_actions, has_many :follow_by_users has_many :follow_user_actions, has_many :follow_users

Counter Cache

ActionStore 内建 Counter Cache 的逻辑(实际上目前是完整统计的,以确保精确),如果你需要,你得在 Target, User 的表里面增加相应的字段。

add_column :users, :star_posts_count, :integer, default: 0
add_column :users, :followers_count, :integer, default: 0
add_column :users, :following_count, :integer, default: 0

add_column :posts, :likes_count, :integer, default: 0
add_column :posts, :stars_count, :integer, default: 0

add_column :comments, :likes_count, :integer, default: 0

于是你就可以这么用了:

@user -> like @post

irb> User.create_action(:like, target: @post, user: @user)
true
irb> @user.create_action(:like, target: @post)
true
irb> @post.reload.likes_count
1

@user1 -> unlike @user2

irb> User.destroy_action(:follow, target: @post, user: @user)
true
irb> @user.destroy_action(:like, target: @post)
true
irb> @post.reload.likes_count
0

Check @user1 is liked @post

irb> action = User.find_action(:follow, target: @post, user: @user)
irb> action = @user.find_action(:like, target: @post)
irb> action.present?
true

User follow cases:

# @user1 -> follow @user2
@user1.create_action(:follow, target: @user2)
@user1.reload.following_count => 1
@user2.reload.followers_count_ => 1
@user1.follow_user?(@user2) => true
# @user2 -> follow @user1
@user2.create_action(:follow, target: @user1)
@user2.follow_user?(@user1) => true
# @user1 -> follow @user3
@user1.create_action(:follow, target: @user3)
# @user1 -> unfollow @user3
 @user1.destroy_action(:follow, target: @user3)

内建关联关系和函数

当你用 action_store 定义 actions 以后,ActionStore 会自动创建 Target 和 User 的多对多关联关系。

例如:

class User < ActiveRecord::Base
  action_store :like, :post
  action_store :block, :user
end

会定义 Many-to-Many 关系:

  • For User model will defined: <action>_<target>s (like_posts)
  • For Target model will defined: <action>_by_users (like_by_users)
# for User model
has_many :like_post_actions
has_many :like_posts, through: :like_post_actions
## as user
has_many :block_user_actions
has_many :block_users, through: :block_user_actions
## as target
has_many :block_by_user_actions
has_many :block_by_users, through: :block_by_user_actions

# for Target model
has_many :like_by_user_actions
has_many :like_by_users, through: :like_user_actions

同时 User model 将会得到这些函数:

@user.create_action(:like, target: @post)
@user.destroy_action(:like, target: @post)
@user.find_action(:like, target: @post)
@user.like_post(@post)
@user.like_post?(@post)
@user.unlike_post(@post)
@user.block_user(@user1)
@user.unblock_user(@user1)
@user.like_post_ids
@user.block_user_ids
@user.block_by_user_ids

此方法将会替代 Ruby China 里面的类似功能,参见这个 ruby-china/homeland#857

共收到 32 条回复
8546

写的这么详细,👍一个

3 lgn21st 将本帖设为了精华贴 2月08日 10:35
2564

是不是没写怎么run migrate

2564

#6楼 @huacnlee 😀 我试一下,看起来目前只能用 User ? Member或其他的不行?

2564

#7楼 @huacnlee great,用上了,感觉很方便

96

马上试下

648

之前也想写一个,被你占先啦

6828

还准备以后把项目里一个类似的功能提取出来发布呢,结果已被捷足先登了👏

1553

👍勤劳的小蜜蜂

2

#12楼 @cao7113 #14楼 @phun

你们的做法也是这样吗?发出来探讨一下

648

#16楼 @huacnlee 类似思路,远没你的丰富翔实,大大的👍,准备采用

2

#17楼 @cao7113 😄 以后不要等我,Just do it!

6828

#16楼 @huacnlee 我是构建一个 Relation 类,主要有 user_id, action_type 和多态 relationable 等数据。辅助 Relationable module,任何想和 User 发生关系(喜欢,赞等等)的东西(比如 Post,Comment) 都可以 include relationable,会获得一些糖方法。这样把 relation 当做 resource, 相关的操作就可以集中放到 relations_controller 里,route 里也简单了很多。顺便把 relation 的 view 也抽象出了一个 relation_tag helper,在显示的时候可以用 ajax,配合 controller 里的 refresh 方法,这样缓存时就能整个缓存一个统一的 item,等页面加载完再根据当前用户更新其 relation 情况。

等于是做一个包含 MVC 的 relationable 库,功能比较重,多指教!

2

#19楼 @phun 哦,那看起来最后大家都是想到一块儿了,中间表结构的思路都是差不多的。 😄

Controller, View Helper 没做,因为实际情况(例如 Ruby China 的场景)可能在 like_xxx 的过程有其他动作(创建通知,Event 之类的),所以就没有像其他 rails-engine 里面的 Gem 那样把整个链路给实现。

20195

厉害了👍

2880

其实用 pg 的 LISTEN / UNLISTEN / NOTIFY 语句做订阅说不定更高效

2

#22楼 @luikore 呀,可以试试这个东西,用来替代 Ruby China 的某些功能。

这个可以配合 ActionStore 一起用呀! 我目前实现这个目的是为了解决数据存储(持久化)的需求,同时保持 MySQL 也可以用。

2188

大大的顶。👍 👍

3773

我是 acts_as_votable 的深度使用者, 发现楼主这个,和这个gem, 有很多类似的地方

2

#26楼 @ginchenorlee 😅 我想一把支持更多

24996

不错代码级的复用,对于有多种相同业务的公司,还可以采用服务级复用.做成serverless的模式.

9442

https://github.com/chrome/markable 我做的rails5兼容版本不小心被我删除了。

30楼 已删除
186

这名字,一开始想,ActionStore 这不应该是一个 Rails 负责存储层的一个子框架吗。好吧,其实是存 Action 的 Store

28215

@huacnlee 你好,我想请问下这个,

attr_reader :defined_actions

def find_defined_action(action_type, target_type)
   action_type = action_type.to_s
   name = target_type.to_s.singularize.underscore
   defined_actions.find do |a|
     a[:action_type] == action_type && (a[:action_name] == name || a[:target_type] == target_type)
   end
end

其中defined_actions的值是如何传递进去的?我的理解是attr_reader方法只是生成了一个可以读取对应实例变量的实例方法,和defined_actions这里的直接调用并不对应啊,感谢!

2
28215renyijiu 回复
action_store :like, :post, counter_cache: true

调用 action_store 定义 Action 的时候,动态创建的

1466

感觉很好用啊,多谢啦~

有个问题,就比如RubyChina的Topic#show页面这样的场景:当前用户赞了很多个Reply,在显示的时候已经赞过的Reply要标记成红色,这个数据有什么方便的方法能includes进来?感觉一条条reply去判断 current_user.like_reply?(reply) 会有很多条查询,但是includes进来所有like 的数据有又觉得没必要。

唔,看了下RubyChina的源码,大概了解了

Controller:

@user_like_reply_ids = current_user&.like_reply_ids_by_replies(@replies) || []

Model:

def like_reply_ids_by_replies(replies)
  return [] if replies.blank?
  return [] if self.like_reply_ids.blank?
  # Intersection between reply ids and user like_reply_ids
  self.like_reply_actions.where(target_id: replies.collect(&:id)).pluck(:target_id)
end

可以解决问题,但感觉有一点不好的是,我现在要做页面同时有“顶”和“踩”…这个步骤我得重复一遍 😂

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册