package io.kaif.model.vote;
import static java.util.stream.Collectors.*;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import javax.annotation.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
import com.google.common.collect.ImmutableMap;
import io.kaif.database.DaoOperations;
import io.kaif.flake.FlakeId;
import io.kaif.model.article.Article;
import io.kaif.model.article.ArticleDao;
@Repository
public class VoteDao implements DaoOperations {
@Autowired
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Autowired
private ArticleDao articleDao;
private final RowMapper<ArticleVoter> articleVoterMapper = (rs, rowNum) -> {
return new ArticleVoter(UUID.fromString(rs.getString("voterId")),
FlakeId.valueOf(rs.getLong("articleId")),
VoteState.valueOf(rs.getString("voteState")),
rs.getLong("previousCount"),
rs.getTimestamp("updateTime").toInstant());
};
private final RowMapper<DebateVoter> debateVoterMapper = (rs, rowNum) -> {
return new DebateVoter(UUID.fromString(rs.getString("voterId")),
FlakeId.valueOf(rs.getLong("articleId")),
FlakeId.valueOf(rs.getLong("debateId")),
VoteState.valueOf(rs.getString("voteState")),
rs.getLong("previousCount"),
rs.getTimestamp("updateTime").toInstant());
};
@Override
public NamedParameterJdbcTemplate namedJdbc() {
return namedParameterJdbcTemplate;
}
public List<ArticleVoter> listArticleVotersInRage(UUID accountId,
FlakeId oldestArticleId,
FlakeId newestArticleId) {
return jdbc().query(""
+ " SELECT * "
+ " FROM ArticleVoter "
+ " WHERE voterId = ? "
+ " AND articleId BETWEEN ? AND ? ",
articleVoterMapper,
accountId,
oldestArticleId.value(),
newestArticleId.value());
}
public void voteDebate(VoteState newState,
FlakeId articleId,
FlakeId debateId,
UUID voterId,
VoteState previousState,
long previousCount,
Instant now) throws DuplicateKeyException {
DebateVoter voter = DebateVoter.create(newState,
articleId,
debateId,
voterId,
previousCount,
now);
// allow two cases:
//
// 1) no data: just do INSERT
// 2) a vote exist match previous state: update to new vote state
//
// note that we do not allow update previousState not match.
String upsert = ""
+ " INSERT "
+ " INTO DebateVoter "
+ " (voterId, articleId, debateId, previousCount, updateTime, voteState) "
+ " VALUES (:voterId, :articleId, :debateId, :previousCount, :updateTime, :voteState) "
+ " ON CONFLICT (voterId, articleId, debateId) "
+ " DO UPDATE "
+ " SET previousCount = :previousCount "
+ " , updateTime = :updateTime "
+ " , voteState = :voteState "
+ " WHERE DebateVoter.voteState = :previousState ";
Map<String, Object> params = ImmutableMap.<String, Object>builder().put("voterId",
voter.getVoterId())
.put("debateId", voter.getDebateId().value())
.put("articleId", voter.getArticleId().value())
.put("previousCount", voter.getPreviousCount())
.put("updateTime", Timestamp.from(voter.getUpdateTime()))
.put("voteState", voter.getVoteState().name())
.put("previousState", previousState.name())
.build();
/**
* when inserted, rowAffected = 1
* when conflict update, rowAffected = 1
* when conflict update but previousState not match, rowAffected = 0
*/
final int rowAffected = namedJdbc().update(upsert, params);
if (rowAffected == 0) {
throw new DuplicateKeyException("previousState: "
+ previousState
+ " not match exist DebateVoter.voteState");
}
}
public List<DebateVoter> listDebateVotersByArticle(UUID accountId, FlakeId articleId) {
return jdbc().query(""
+ " SELECT * "
+ " FROM DebateVoter "
+ " WHERE voterId = ? "
+ " AND articleId = ? ", debateVoterMapper, accountId, articleId.value());
}
public void voteArticle(VoteState newState,
FlakeId articleId,
UUID voterId,
VoteState previousState,
long previousCount,
Instant now) throws DuplicateKeyException {
ArticleVoter voter = ArticleVoter.create(newState, articleId, voterId, previousCount, now);
/*
* implementation similar to voteDebate, see document there for explanation
*/
String upsert = ""
+ " INSERT "
+ " INTO ArticleVoter "
+ " (voterId, articleId, previousCount, updateTime, voteState) "
+ " VALUES (:voterId, :articleId, :previousCount, :updateTime, :voteState) "
+ " ON CONFLICT (voterid, articleId) "
+ " DO UPDATE "
+ " SET voteState = :voteState "
+ " , previousCount = :previousCount "
+ " , updateTime = :updateTime "
+ " WHERE ArticleVoter.voteState = :previousState ";
Map<String, Object> params = ImmutableMap.<String, Object>builder().put("voterId",
voter.getVoterId())
.put("articleId", voter.getArticleId().value())
.put("previousCount", voter.getPreviousCount())
.put("updateTime", Timestamp.from(voter.getUpdateTime()))
.put("voteState", voter.getVoteState().name())
.put("previousState", previousState.name())
.build();
final int rowAffected = namedJdbc().update(upsert, params);
if (rowAffected == 0) {
throw new DuplicateKeyException("previousState: "
+ previousState
+ " not match exist ArticleVoter.voteState");
}
}
public List<ArticleVoter> listArticleVoters(UUID voterId, List<FlakeId> articleIds) {
if (articleIds.isEmpty()) {
return Collections.emptyList();
}
Map<String, Object> params = ImmutableMap.of("voterId",
voterId,
"articleIds",
articleIds.stream().map(FlakeId::value).collect(toList()));
return namedJdbc().query(""
+ " SELECT * "
+ " FROM ArticleVoter "
+ " WHERE voterId = :voterId "
+ " AND articleId IN (:articleIds) ", params, articleVoterMapper);
}
public List<DebateVoter> listDebateVotersByIds(UUID voterId, List<FlakeId> debateIds) {
if (debateIds.isEmpty()) {
return Collections.emptyList();
}
Map<String, Object> params = ImmutableMap.of("voterId",
voterId,
"debateIds",
debateIds.stream().map(FlakeId::value).collect(toList()));
return namedJdbc().query(""
+ " SELECT * "
+ " FROM DebateVoter"
+ " WHERE voterId = :voterId "
+ " AND debateId IN (:debateIds) ", params, debateVoterMapper);
}
/**
* up voted articles are private to voter, so we don't exclude deleted articles
*/
public List<Article> listUpVotedArticles(UUID voterId,
@Nullable FlakeId startArticleId,
int size) {
FlakeId start = Optional.ofNullable(startArticleId).orElse(FlakeId.MAX);
return jdbc().query(""
+ " SELECT a.* "
+ " FROM ArticleVoter v "
+ " JOIN Article a ON (v.articleId = a.articleId) "
+ " WHERE v.voterId = ? "
+ " AND v.articleId < ? "
+ " AND v.voteState = ? "
+ " ORDER BY v.articleId DESC "
+ " LIMIT ? ",
articleDao.getArticleMapper(),
voterId,
start.value(),
VoteState.UP.name(),
size);
}
}