From 0c0ea8897e64461b1cfa9cd86a939b48f0bdd640 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Sat, 3 Dec 2022 15:28:06 +0300 Subject: Initial PostgreSQL schema and profile --- .../com/juick/service/MessagesServiceImpl.java | 206 +++++++++++++-------- 1 file changed, 129 insertions(+), 77 deletions(-) (limited to 'src/main/java/com/juick/service/MessagesServiceImpl.java') diff --git a/src/main/java/com/juick/service/MessagesServiceImpl.java b/src/main/java/com/juick/service/MessagesServiceImpl.java index 8251217f..c5037c31 100644 --- a/src/main/java/com/juick/service/MessagesServiceImpl.java +++ b/src/main/java/com/juick/service/MessagesServiceImpl.java @@ -38,18 +38,18 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.lang.NonNull; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; -import javax.annotation.Nonnull; import javax.inject.Inject; import java.net.URI; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; -import java.sql.Types; import java.time.Instant; +import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; import java.util.*; import java.util.stream.Collectors; @@ -63,6 +63,8 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ @Inject private UserService userService; @Inject + private TagService tagService; + @Inject private SearchService searchService; @Inject private StorageService storageService; @@ -93,8 +95,8 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ msg.setReplies(rs.getInt(10)); msg.setAttachmentType(rs.getString(11)); msg.Hidden = rs.getBoolean(13); - String tagsStr = StringUtils.defaultString(rs.getString(14)); - msg.setTags(MessageUtils.parseTags(tagsStr)); + msg.setTags(tagService.getMessageTags(msg.getMid()).stream() + .map(tag -> tag.getTag()).collect(Collectors.toSet())); msg.setRepliesBy(rs.getString(15)); msg.setText(rs.getString(16)); msg.setReplyQuote(MessageUtils.formatQuote(rs.getString(17))); @@ -132,17 +134,17 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ */ @Transactional @Override - public int createMessage(final int uid, final String txt, final String attachment, @Nonnull final Set tags) { + public int createMessage(final int uid, final String txt, final String attachment, @NonNull final Set tags) { SimpleJdbcInsert simpleJdbcInsert = new SimpleJdbcInsert(getJdbcTemplate()).withTableName("messages") .usingColumns("user_id", "attach", "ts", "readonly").usingGeneratedKeyColumns("message_id"); - Map insertMap = new HashMap<>(); - insertMap.put("user_id", uid); - Instant now = Instant.now(); - insertMap.put("ts", Timestamp.from(now)); + var insertMap = new MapSqlParameterSource(); + insertMap.addValue("user_id", uid); + var now = Instant.now(); + insertMap.addValue("ts", now.atOffset(ZoneOffset.UTC), java.sql.Types.TIMESTAMP_WITH_TIMEZONE); if (StringUtils.isNotEmpty(attachment)) { - insertMap.put("attach", attachment); + insertMap.addValue("attach", attachment); } - insertMap.put("readonly", TagUtils.hasTag(tags, "readonly")); + insertMap.addValue("readonly", TagUtils.hasTag(tags, "readonly")); int mid = simpleJdbcInsert.executeAndReturnKey(insertMap).intValue(); if (mid > 0) { if (CollectionUtils.isNotEmpty(tags)) { @@ -150,7 +152,7 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ getJdbcTemplate().batchUpdate("INSERT INTO messages_tags(message_id, tag_id) VALUES (?, ?)", new BatchPreparedStatementSetter() { @Override - public void setValues(@Nonnull PreparedStatement ps, int i) throws SQLException { + public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setInt(1, mid); ps.setInt(2, newTags.get(i).TID); } @@ -161,13 +163,22 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ } }); } - getJdbcTemplate().update("INSERT INTO messages_txt(message_id, txt, updated_at) VALUES (?, ?, ?)", - new Object[] { mid, txt, Timestamp.from(now) }, - new int[] { Types.INTEGER, Types.VARCHAR, Types.TIMESTAMP }); - getJdbcTemplate().update("UPDATE users SET lastmessage=?, last_seen=? where id=?", Timestamp.from(now), - Timestamp.from(now), uid); + getNamedParameterJdbcTemplate() + .update("INSERT INTO messages_txt(message_id, txt, updated_at) VALUES (:mid, :txt, :now)", + new MapSqlParameterSource() + .addValue("mid", mid) + .addValue("txt", txt) + .addValue("now", now.atOffset(ZoneOffset.UTC), + java.sql.Types.TIMESTAMP_WITH_TIMEZONE)); + getNamedParameterJdbcTemplate() + .update("UPDATE users SET lastmessage=:lastmessage, last_seen=:last_seen where id=:uid", + new MapSqlParameterSource() + .addValue("lastmessage", now.atOffset(ZoneOffset.UTC), + java.sql.Types.TIMESTAMP_WITH_TIMEZONE) + .addValue("last_seen", now.atOffset(ZoneOffset.UTC), + java.sql.Types.TIMESTAMP_WITH_TIMEZONE) + .addValue("uid", uid)); } - return mid; } @@ -187,16 +198,40 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ public int createReply(final int mid, final int rid, final User user, final String txt, final String attachment) { int ridnew = getReplyIDIncrement(mid, user.getUid()); if (ridnew > 0) { - Timestamp ts = Timestamp.from(Instant.now()); - getJdbcTemplate().update( - "INSERT INTO replies(message_id, reply_id, user_id, replyto, attach, txt, ts, updated_at, user_uri) " - + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", - mid, ridnew, user.getUid(), rid, attachment, txt, ts, ts, user.getUri().toASCIIString()); - - getJdbcTemplate().update("UPDATE messages SET replies = replies + 1, updated=? WHERE message_id = ?", ts, - mid); + var now = Instant.now(); + getNamedParameterJdbcTemplate() + .update( + "INSERT INTO replies(message_id, reply_id, user_id, replyto, attach, txt, ts, updated_at, user_uri) " + + "VALUES (:mid, :rid, :uid, :replyto, :attach, :txt, :ts, :updated_at, :user_uri)", + new MapSqlParameterSource() + .addValue("mid", mid) + .addValue("rid", ridnew) + .addValue("uid", user.getUid()) + .addValue("replyto", rid) + .addValue("attach", attachment) + .addValue("txt", txt) + .addValue("ts", now.atOffset(ZoneOffset.UTC), + java.sql.Types.TIMESTAMP_WITH_TIMEZONE) + .addValue("updated_at", now.atOffset(ZoneOffset.UTC), + java.sql.Types.TIMESTAMP_WITH_TIMEZONE) + .addValue("user_uri", user.getUri().toASCIIString())); + + getNamedParameterJdbcTemplate() + .update( + "UPDATE messages SET replies = replies + 1, updated=:updated WHERE message_id = :message_id", + new MapSqlParameterSource() + .addValue("updated", now.atOffset(ZoneOffset.UTC), + java.sql.Types.TIMESTAMP_WITH_TIMEZONE) + .addValue("message_id", mid)); setLastReadComment(user, mid, ridnew); - getJdbcTemplate().update("UPDATE users SET lastmessage=?, last_seen=? where id=?", ts, ts, user.getUid()); + getNamedParameterJdbcTemplate() + .update("UPDATE users SET lastmessage=:lastmessage, last_seen=:last_seen where id=:uid", + new MapSqlParameterSource() + .addValue("lastmessage", now.atOffset(ZoneOffset.UTC), + java.sql.Types.TIMESTAMP_WITH_TIMEZONE) + .addValue("last_seen", now.atOffset(ZoneOffset.UTC), + java.sql.Types.TIMESTAMP_WITH_TIMEZONE) + .addValue("uid", user.getUid())); } return ridnew; } @@ -205,7 +240,7 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ return getJdbcTemplate().execute((ConnectionCallback) conn -> { conn.setAutoCommit(false); int replyNo; - final int readOnly; + final boolean readOnly; final int userId; try (PreparedStatement ps = conn.prepareStatement( "SELECT maxreplyid+1, readonly, user_id FROM messages WHERE message_id=? FOR UPDATE")) { @@ -213,7 +248,7 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ try (ResultSet resultSet = ps.executeQuery()) { if (resultSet.next()) { replyNo = resultSet.getInt(1); - readOnly = resultSet.getInt(2); + readOnly = resultSet.getBoolean(2); userId = resultSet.getInt(3); } else { throw new IncorrectResultSizeDataAccessException( @@ -222,7 +257,7 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ } } // author can reply to his readonly post - if (readOnly == 0 || uid == userId) { + if (!readOnly || uid == userId) { try (PreparedStatement ps = conn .prepareStatement("UPDATE messages SET maxreplyid=? WHERE message_id=?")) { ps.setInt(1, replyNo); @@ -308,7 +343,8 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ } } boolean wasAdded = getJdbcTemplate().update( - "INSERT INTO favorites(user_id, message_id, ts, like_id, user_uri) VALUES (?, ?, NOW(), ?, ?)", vuid, + "INSERT INTO favorites(user_id, message_id, ts, like_id, user_uri) VALUES (?, ?, NOW(), ?, ?)", + vuid, mid, reaction, userUri) == 1; if (wasAdded) { return RecommendStatus.Added; @@ -377,7 +413,7 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ + "messages.user_id as uid, users.nick, users.banned as banned, " + "" + "messages.ts," + "messages.readonly, messages.privacy, messages.replies," + "messages.attach, COUNT(DISTINCT favorites.user_id) as likes, messages.hidden," - + "GROUP_CONCAT(tags.name SEPARATOR ' '), txt.repliesby, txt.txt, '' as q, messages.updated as updated, 0 as to_uid, " + + "'' as tags, txt.repliesby, txt.txt, '' as q, messages.updated as updated, 0 as to_uid, " + "NULL as to_name, txt.updated_at, '' as reply_user_uri, '' as to_uri, '' as reply_uri, 0 as html, 0 as unread FROM messages " + "INNER JOIN users ON messages.user_id = users.id " + "INNER JOIN messages_txt AS txt " + "ON messages.message_id = txt.message_id " + "LEFT JOIN favorites " @@ -386,7 +422,7 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ + "LEFT JOIN tags ON tags.tag_id=messages_tags.tag_id " + "WHERE messages.message_id = ? AND users.banned = 0 " + "GROUP BY mid, rid, replyto, uid, nick, banned, messages.ts, readonly, " - + "privacy, replies, attach, repliesby, q, updated_at, reply_user_uri, to_uri, reply_uri, html, unread", + + "privacy, replies, attach, repliesby, q, txt.txt, updated_at, reply_user_uri, to_uri, reply_uri, html, unread", new MessageMapper(), mid); if (!list.isEmpty()) { final Message message = list.get(0); @@ -407,7 +443,7 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ @Override public Message getReply(final int mid, final int rid) { List list = getJdbcTemplate().query("SELECT replies.user_id, users.nick," - + "replies.replyto, replies.ts," + "replies.attach, replies.txt, IFNULL(q.txt,t.txt) as quote, " + + "replies.replyto, replies.ts," + "replies.attach, replies.txt, COALESCE(q.txt,t.txt) as quote, " + "COALESCE(q.user_id, m.user_id) AS to_uid, COALESCE(qu.nick, mu.nick) AS to_name, " + "replies.updated_at, replies.user_uri as uri, " + "q.user_uri AS to_uri, replies.reply_uri AS reply_uri, replies.html, q.reply_uri " @@ -774,30 +810,30 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ if (CollectionUtils.isNotEmpty(mids)) { var query = "WITH RECURSIVE banned(message_id, reply_id) " - + "AS (SELECT message_id, reply_id FROM replies WHERE replies.message_id IN (:ids) " - + "AND (EXISTS (SELECT 1 FROM bl_users b WHERE b.user_id = :uid AND b.bl_user_id = replies.user_id) " - + "OR EXISTS (SELECT 1 from users u WHERE u.banned = 1 and u.id = replies.user_id and u.id <> :uid)) " - + "UNION ALL SELECT replies.message_id, replies.reply_id FROM replies INNER JOIN banned " - + "ON banned.reply_id = replies.replyto AND replies.reply_id != replies.replyto AND banned.message_id=replies.message_id " - + "WHERE replies.message_id IN (:ids)) " + "SELECT messages.message_id, 0 as rid, 0 as replyto, " - + "messages.user_id,users.nick, 0 as usr_banned, " + "messages.ts," - + "messages.readonly,messages.privacy, messages.replies-COUNT(DISTINCT banned.reply_id) as replies," - + "messages.attach,COUNT(DISTINCT favorites.user_id) AS likes,messages.hidden," - + "GROUP_CONCAT(DISTINCT tags.name SEPARATOR ' '), messages_txt.repliesby, messages_txt.txt, '' as q, " - + "messages.updated, 0 as to_uid, NULL as to_name, messages_txt.updated_at, '' as m_user_uri, " - + "'' as to_uri, '' as msg_reply_uri, 0 as html, cast(messages.replies as signed)-cast(subscr_messages.last_read_rid as signed) > 0 as unread " - + "FROM (messages INNER JOIN messages_txt " + "ON messages.message_id=messages_txt.message_id) " - + "INNER JOIN users ON messages.user_id=users.id " + "LEFT JOIN subscr_messages " - + "ON messages.message_id=subscr_messages.message_id AND subscr_messages.suser_id=:uid " - + "LEFT JOIN favorites " + "ON messages.message_id = favorites.message_id AND favorites.like_id=1 " - + "LEFT JOIN banned " + "ON messages.message_id = banned.message_id " - + "LEFT JOIN messages_tags ON messages_tags.message_id=messages_txt.message_id " - + "LEFT JOIN tags ON tags.tag_id=messages_tags.tag_id " - + "WHERE messages.message_id IN (:ids) GROUP BY " - + "messages.message_id, rid, replyto, messages.user_id, users.nick, usr_banned, messages.ts, " - + "messages.readonly, messages.privacy, messages.attach, messages.hidden, " - + "messages_txt.repliesby, messages_txt.txt, q, messages.updated, to_uid, to_name, updated_at, " - + "m_user_uri, msg_reply_uri, html"; + + "AS (SELECT message_id, reply_id FROM replies WHERE replies.message_id IN (:ids) " + + "AND (EXISTS (SELECT 1 FROM bl_users b WHERE b.user_id = :uid AND b.bl_user_id = replies.user_id) " + + "OR EXISTS (SELECT 1 from users u WHERE u.banned = 1 and u.id = replies.user_id and u.id <> :uid)) " + + "UNION ALL SELECT replies.message_id, replies.reply_id FROM replies INNER JOIN banned " + + "ON banned.reply_id = replies.replyto AND replies.reply_id != replies.replyto AND banned.message_id=replies.message_id " + + "WHERE replies.message_id IN (:ids)) " + "SELECT messages.message_id, 0 as rid, 0 as replyto, " + + "messages.user_id,users.nick, 0 as usr_banned, " + "messages.ts," + + "messages.readonly,messages.privacy, messages.replies-COUNT(DISTINCT banned.reply_id) as replies," + + "messages.attach,COUNT(DISTINCT favorites.user_id) AS likes,messages.hidden," + + "'' as tags, messages_txt.repliesby, messages_txt.txt, '' as q, " + + "messages.updated, 0 as to_uid, NULL as to_name, messages_txt.updated_at, '' as m_user_uri, " + + "'' as to_uri, '' as msg_reply_uri, 0 as html, (messages.replies - subscr_messages.last_read_rid) > 0 as unread " + + "FROM (messages INNER JOIN messages_txt " + "ON messages.message_id=messages_txt.message_id) " + + "INNER JOIN users ON messages.user_id=users.id " + "LEFT JOIN subscr_messages " + + "ON messages.message_id=subscr_messages.message_id AND subscr_messages.suser_id=:uid " + + "LEFT JOIN favorites " + "ON messages.message_id = favorites.message_id AND favorites.like_id=1 " + + "LEFT JOIN banned " + "ON messages.message_id = banned.message_id " + + "LEFT JOIN messages_tags ON messages_tags.message_id=messages_txt.message_id " + + "LEFT JOIN tags ON tags.tag_id=messages_tags.tag_id " + + "WHERE messages.message_id IN (:ids) GROUP BY " + + "messages.message_id, rid, replyto, messages.user_id, users.nick, usr_banned, messages.ts, " + + "messages.readonly, messages.privacy, messages.attach, messages.hidden, " + + "messages_txt.repliesby, messages_txt.txt, q, messages.updated, to_uid, to_name, updated_at, " + + "m_user_uri, msg_reply_uri, html, subscr_messages.last_read_rid"; List msgs = getNamedParameterJdbcTemplate().query(query, new MapSqlParameterSource("ids", mids).addValue("uid", visitor.getUid()), new MessageMapper()); @@ -830,7 +866,7 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ return getNamedParameterJdbcTemplate().query( "select f.message_id as mid, f.like_id as lid," + " r.description as descr, count(f.like_id) as cnt" + " from favorites f LEFT JOIN reactions r ON f.like_id = r.like_id " - + " where f.message_id IN (:mids) " + " group by f.message_id, f.like_id", + + " where f.message_id IN (:mids) " + " group by f.message_id, f.like_id, r.description", new MapSqlParameterSource("mids", mids), (ResultSet rs) -> { Map> results = new HashMap<>(); @@ -862,7 +898,7 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ + "SELECT replies.message_id as mid, replies.reply_id, replies.replyto, " + "replies.user_id, users.nick, users.banned, " + "replies.ts, " + "0 as readonly, 0 as privacy, 0 as replies, " + "replies.attach, 0 as likes, 0 as hidden, " - + "NULL as tags, NULL as repliesby, replies.txt, " + "IFNULL(qw.txt, t.txt) as q, " + "NOW(), " + + "NULL as tags, NULL as repliesby, replies.txt, " + "COALESCE(qw.txt, t.txt) as q, " + "NOW(), " + "COALESCE(qw.user_id, m.user_id) as to_uid, COALESCE(qu.nick, mu.nick) as to_name, " + "replies.updated_at, replies.user_uri as uri, " + "qw.user_uri as to_uri, replies.reply_uri, replies.html, 0 as unread " @@ -880,7 +916,7 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ if (replies.size() > 0 && !user.isAnonymous()) { setRead(user, mid); } - replies.forEach(i -> { + replies.forEach(i -> { i.setEntities(MessageUtils.getEntities(i)); i.getUser().setAvatar(webApp.getAvatarUrl(i.getUser())); }); @@ -967,8 +1003,14 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ @Transactional(readOnly = true) @Override public List getLastMessages(int hours) { - return getJdbcTemplate().queryForList( - "SELECT message_id FROM messages WHERE messages.ts>TIMESTAMPADD(HOUR,?,NOW())", Integer.class, -hours); + return getNamedParameterJdbcTemplate() + .queryForList( + "SELECT message_id FROM messages WHERE messages.ts > :hours", + new MapSqlParameterSource() + .addValue("hours", + Instant.now().minus(hours, ChronoUnit.HOURS).atOffset(ZoneOffset.UTC), + java.sql.Types.TIMESTAMP_WITH_TIMEZONE), + Integer.class); } @@ -997,18 +1039,27 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ @Transactional(readOnly = true) @Override public List getPopularCandidates() { - return getJdbcTemplate().queryForList("SELECT replies.message_id FROM replies " - + "INNER JOIN messages ON replies.message_id = messages.message_id " - + "LEFT JOIN favorites ON favorites.message_id = messages.message_id " - + "LEFT JOIN messages_tags ON messages_tags.message_id = messages.message_id " - + "WHERE COALESCE(messages_tags.tag_id, 0) != 2 " - + "AND COALESCE(messages_tags.tag_id, 0) != 805 AND replies.ts > TIMESTAMPADD(HOUR, -2, CURRENT_TIMESTAMP) " - + "AND NOT EXISTS (SELECT 1 FROM favorites WHERE message_id = messages.message_id AND user_id = 2) GROUP BY messages.message_id having COUNT(DISTINCT(replies.user_id)) > 5 " - + "UNION ALL SELECT favorites.message_id FROM favorites " - + "INNER JOIN messages ON messages.message_id = favorites.message_id " - + "LEFT JOIN messages_tags ON messages_tags.message_id = messages.message_id " - + "WHERE COALESCE(messages_tags.tag_id, 0) != 2 AND favorites.ts > TIMESTAMPADD(HOUR, -2, CURRENT_TIMESTAMP) " - + "AND NOT EXISTS (SELECT 1 FROM favorites WHERE message_id = messages.message_id AND user_id = 2) GROUP BY messages.message_id HAVING COUNT(DISTINCT favorites.user_id) > 2;", + var beforeTime = Instant.now().minus(2, ChronoUnit.HOURS); + var sql = """ + SELECT replies.message_id FROM replies + INNER JOIN messages ON replies.message_id = messages.message_id + LEFT JOIN favorites ON favorites.message_id = messages.message_id + LEFT JOIN messages_tags ON messages_tags.message_id = messages.message_id + WHERE COALESCE(messages_tags.tag_id, 0) != 2 + AND COALESCE(messages_tags.tag_id, 0) != 805 AND replies.ts > :before + AND NOT EXISTS (SELECT 1 FROM favorites WHERE message_id = messages.message_id + AND user_id = 2) GROUP BY replies.message_id HAVING COUNT(DISTINCT(replies.user_id)) > 5 + UNION ALL SELECT favorites.message_id FROM favorites + INNER JOIN messages ON messages.message_id = favorites.message_id + LEFT JOIN messages_tags ON messages_tags.message_id = messages.message_id + WHERE COALESCE(messages_tags.tag_id, 0) != 2 + AND favorites.ts > :before + AND NOT EXISTS (SELECT 1 FROM favorites + WHERE message_id = messages.message_id AND user_id = 2) + GROUP BY favorites.message_id HAVING COUNT(DISTINCT favorites.user_id) > 2 + """; + return getNamedParameterJdbcTemplate().queryForList(sql, new MapSqlParameterSource() + .addValue("before", beforeTime.atOffset(ZoneOffset.UTC), java.sql.Types.TIMESTAMP_WITH_TIMEZONE), Integer.class); } @@ -1111,11 +1162,12 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ SqlParameterSource parameterSource = new MapSqlParameterSource().addValue("mid", mid).addValue("rid", rid) .addValue("key", key).addValue("value", value); if (StringUtils.isNotEmpty(value)) { - try { + var exists = getMessageProperty(mid, rid, key); + if (StringUtils.isEmpty(exists)) { getNamedParameterJdbcTemplate() .update("INSERT INTO messages_properties(message_id, reply_id, property_key, property_value) " + "VALUES(:mid, :rid, :key, :value)", parameterSource); - } catch (DataIntegrityViolationException ex) { + } else { getNamedParameterJdbcTemplate().update("UPDATE messages_properties SET property_value=:value " + "WHERE message_id=:mid AND reply_id=:rid AND property_key=:key", parameterSource); } -- cgit v1.2.3