Migrate to MyBatis with proper Controller→Service→Mapper layering
- Add MyBatis Spring Boot Starter with XML mappers and domain classes - Create 9 mapper interfaces + XML: Restaurant, Video, Channel, Review, User, Stats, DaemonConfig, Search, Vector - Create 10 domain classes with Lombok: Restaurant, VideoSummary, VideoDetail, VideoRestaurantLink, Channel, Review, UserInfo, DaemonConfig, SiteVisitStats, VectorSearchResult - Create 7 new service classes: RestaurantService, VideoService, ChannelService, ReviewService, UserService, StatsService, DaemonConfigService - Refactor all controllers to be thin (HTTP + auth only), delegating business logic to services - Refactor SearchService, PipelineService, DaemonScheduler, AuthService, YouTubeService to use mappers/services instead of JDBC/repositories - Add Jackson SNAKE_CASE property naming for consistent API responses - Add ClobTypeHandler for Oracle CLOB→String in MyBatis - Add IdGenerator utility for centralized UUID generation - Delete old repository/ package (6 files), JdbcConfig, LowerCaseKeyAdvice - VectorService retains JDBC for Oracle VECTOR type support Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.tasteby.mapper.ChannelMapper">
|
||||
|
||||
<resultMap id="channelResultMap" type="com.tasteby.domain.Channel">
|
||||
<id property="id" column="id"/>
|
||||
<result property="channelId" column="channel_id"/>
|
||||
<result property="channelName" column="channel_name"/>
|
||||
<result property="titleFilter" column="title_filter"/>
|
||||
<result property="videoCount" column="video_count"/>
|
||||
<result property="lastVideoAt" column="last_video_at"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="findAllActive" resultMap="channelResultMap">
|
||||
SELECT c.id, c.channel_id, c.channel_name, c.title_filter,
|
||||
(SELECT COUNT(*) FROM videos v WHERE v.channel_id = c.id) AS video_count,
|
||||
(SELECT MAX(v.published_at) FROM videos v WHERE v.channel_id = c.id) AS last_video_at
|
||||
FROM channels c
|
||||
WHERE c.is_active = 1
|
||||
ORDER BY c.channel_name
|
||||
</select>
|
||||
|
||||
<insert id="insert">
|
||||
INSERT INTO channels (id, channel_id, channel_name, title_filter)
|
||||
VALUES (#{id}, #{channelId}, #{channelName}, #{titleFilter})
|
||||
</insert>
|
||||
|
||||
<update id="deactivateByChannelId">
|
||||
UPDATE channels SET is_active = 0
|
||||
WHERE channel_id = #{channelId} AND is_active = 1
|
||||
</update>
|
||||
|
||||
<update id="deactivateById">
|
||||
UPDATE channels SET is_active = 0
|
||||
WHERE id = #{id} AND is_active = 1
|
||||
</update>
|
||||
|
||||
<select id="findByChannelId" resultMap="channelResultMap">
|
||||
SELECT id, channel_id, channel_name, title_filter
|
||||
FROM channels
|
||||
WHERE channel_id = #{channelId} AND is_active = 1
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.tasteby.mapper.DaemonConfigMapper">
|
||||
|
||||
<resultMap id="daemonConfigResultMap" type="com.tasteby.domain.DaemonConfig">
|
||||
<id property="id" column="id"/>
|
||||
<result property="scanEnabled" column="scan_enabled" javaType="boolean"/>
|
||||
<result property="scanIntervalMin" column="scan_interval_min"/>
|
||||
<result property="processEnabled" column="process_enabled" javaType="boolean"/>
|
||||
<result property="processIntervalMin" column="process_interval_min"/>
|
||||
<result property="processLimit" column="process_limit"/>
|
||||
<result property="lastScanAt" column="last_scan_at"/>
|
||||
<result property="lastProcessAt" column="last_process_at"/>
|
||||
<result property="updatedAt" column="updated_at"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="getConfig" resultMap="daemonConfigResultMap">
|
||||
SELECT id, scan_enabled, scan_interval_min, process_enabled, process_interval_min,
|
||||
process_limit, last_scan_at, last_process_at, updated_at
|
||||
FROM daemon_config
|
||||
WHERE id = 1
|
||||
</select>
|
||||
|
||||
<update id="updateConfig">
|
||||
UPDATE daemon_config
|
||||
<set>
|
||||
scan_enabled = #{scanEnabled, javaType=boolean, jdbcType=NUMERIC},
|
||||
scan_interval_min = #{scanIntervalMin},
|
||||
process_enabled = #{processEnabled, javaType=boolean, jdbcType=NUMERIC},
|
||||
process_interval_min = #{processIntervalMin},
|
||||
process_limit = #{processLimit},
|
||||
updated_at = SYSTIMESTAMP,
|
||||
</set>
|
||||
WHERE id = 1
|
||||
</update>
|
||||
|
||||
<update id="updateLastScan">
|
||||
UPDATE daemon_config SET last_scan_at = SYSTIMESTAMP WHERE id = 1
|
||||
</update>
|
||||
|
||||
<update id="updateLastProcess">
|
||||
UPDATE daemon_config SET last_process_at = SYSTIMESTAMP WHERE id = 1
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,230 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.tasteby.mapper.RestaurantMapper">
|
||||
|
||||
<!-- ===== Result Maps ===== -->
|
||||
|
||||
<resultMap id="restaurantMap" type="Restaurant">
|
||||
<id property="id" column="id"/>
|
||||
<result property="name" column="name"/>
|
||||
<result property="address" column="address"/>
|
||||
<result property="region" column="region"/>
|
||||
<result property="latitude" column="latitude"/>
|
||||
<result property="longitude" column="longitude"/>
|
||||
<result property="cuisineType" column="cuisine_type"/>
|
||||
<result property="priceRange" column="price_range"/>
|
||||
<result property="phone" column="phone"/>
|
||||
<result property="website" column="website"/>
|
||||
<result property="googlePlaceId" column="google_place_id"/>
|
||||
<result property="businessStatus" column="business_status"/>
|
||||
<result property="rating" column="rating"/>
|
||||
<result property="ratingCount" column="rating_count"/>
|
||||
<result property="updatedAt" column="updated_at"/>
|
||||
</resultMap>
|
||||
|
||||
<!-- ===== Queries ===== -->
|
||||
|
||||
<select id="findAll" resultMap="restaurantMap">
|
||||
SELECT DISTINCT r.id, r.name, r.address, r.region, r.latitude, r.longitude,
|
||||
r.cuisine_type, r.price_range, r.google_place_id,
|
||||
r.business_status, r.rating, r.rating_count, r.updated_at
|
||||
FROM restaurants r
|
||||
<if test="channel != null and channel != ''">
|
||||
JOIN video_restaurants vr_f ON vr_f.restaurant_id = r.id
|
||||
JOIN videos v_f ON v_f.id = vr_f.video_id
|
||||
JOIN channels c_f ON c_f.id = v_f.channel_id
|
||||
</if>
|
||||
<where>
|
||||
r.latitude IS NOT NULL
|
||||
AND EXISTS (SELECT 1 FROM video_restaurants vr0 WHERE vr0.restaurant_id = r.id)
|
||||
<if test="cuisine != null and cuisine != ''">
|
||||
AND r.cuisine_type = #{cuisine}
|
||||
</if>
|
||||
<if test="region != null and region != ''">
|
||||
AND r.region LIKE '%' || #{region} || '%'
|
||||
</if>
|
||||
<if test="channel != null and channel != ''">
|
||||
AND c_f.channel_name = #{channel}
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY r.updated_at DESC
|
||||
OFFSET #{offset} ROWS FETCH NEXT #{limit} ROWS ONLY
|
||||
</select>
|
||||
|
||||
<select id="findById" resultMap="restaurantMap">
|
||||
SELECT r.id, r.name, r.address, r.region, r.latitude, r.longitude,
|
||||
r.cuisine_type, r.price_range, r.phone, r.website, r.google_place_id,
|
||||
r.business_status, r.rating, r.rating_count
|
||||
FROM restaurants r
|
||||
WHERE r.id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="findVideoLinks" resultType="map">
|
||||
SELECT v.video_id, v.title, v.url, v.published_at,
|
||||
vr.foods_mentioned, vr.evaluation, vr.guests,
|
||||
c.channel_name, c.channel_id
|
||||
FROM video_restaurants vr
|
||||
JOIN videos v ON v.id = vr.video_id
|
||||
JOIN channels c ON c.id = v.channel_id
|
||||
WHERE vr.restaurant_id = #{restaurantId}
|
||||
ORDER BY v.published_at DESC
|
||||
</select>
|
||||
|
||||
<!-- ===== Insert ===== -->
|
||||
|
||||
<insert id="insertRestaurant">
|
||||
INSERT INTO restaurants (id, name, address, region, latitude, longitude,
|
||||
cuisine_type, price_range, google_place_id,
|
||||
phone, website, business_status, rating, rating_count)
|
||||
VALUES (#{id}, #{name}, #{address}, #{region}, #{latitude}, #{longitude},
|
||||
#{cuisineType}, #{priceRange}, #{googlePlaceId},
|
||||
#{phone}, #{website}, #{businessStatus}, #{rating}, #{ratingCount})
|
||||
</insert>
|
||||
|
||||
<!-- ===== Update with COALESCE ===== -->
|
||||
|
||||
<update id="updateRestaurant">
|
||||
UPDATE restaurants SET
|
||||
name = #{name},
|
||||
address = COALESCE(#{address}, address),
|
||||
region = COALESCE(#{region}, region),
|
||||
latitude = COALESCE(#{latitude}, latitude),
|
||||
longitude = COALESCE(#{longitude}, longitude),
|
||||
cuisine_type = COALESCE(#{cuisineType}, cuisine_type),
|
||||
price_range = COALESCE(#{priceRange}, price_range),
|
||||
google_place_id = COALESCE(#{googlePlaceId}, google_place_id),
|
||||
phone = COALESCE(#{phone}, phone),
|
||||
website = COALESCE(#{website}, website),
|
||||
business_status = COALESCE(#{businessStatus}, business_status),
|
||||
rating = COALESCE(#{rating}, rating),
|
||||
rating_count = COALESCE(#{ratingCount}, rating_count),
|
||||
updated_at = SYSTIMESTAMP
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<!-- ===== Dynamic field update ===== -->
|
||||
|
||||
<update id="updateFields">
|
||||
UPDATE restaurants SET
|
||||
<trim suffixOverrides=",">
|
||||
<if test="fields.containsKey('name')">
|
||||
name = #{fields.name},
|
||||
</if>
|
||||
<if test="fields.containsKey('address')">
|
||||
address = #{fields.address},
|
||||
</if>
|
||||
<if test="fields.containsKey('region')">
|
||||
region = #{fields.region},
|
||||
</if>
|
||||
<if test="fields.containsKey('cuisine_type')">
|
||||
cuisine_type = #{fields.cuisine_type},
|
||||
</if>
|
||||
<if test="fields.containsKey('price_range')">
|
||||
price_range = #{fields.price_range},
|
||||
</if>
|
||||
<if test="fields.containsKey('phone')">
|
||||
phone = #{fields.phone},
|
||||
</if>
|
||||
<if test="fields.containsKey('website')">
|
||||
website = #{fields.website},
|
||||
</if>
|
||||
<if test="fields.containsKey('latitude')">
|
||||
latitude = #{fields.latitude},
|
||||
</if>
|
||||
<if test="fields.containsKey('longitude')">
|
||||
longitude = #{fields.longitude},
|
||||
</if>
|
||||
updated_at = SYSTIMESTAMP,
|
||||
</trim>
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<!-- ===== Cascade deletes ===== -->
|
||||
|
||||
<delete id="deleteVectors">
|
||||
DELETE FROM restaurant_vectors WHERE restaurant_id = #{id}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteReviews">
|
||||
DELETE FROM user_reviews WHERE restaurant_id = #{id}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteFavorites">
|
||||
DELETE FROM user_favorites WHERE restaurant_id = #{id}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteVideoRestaurants">
|
||||
DELETE FROM video_restaurants WHERE restaurant_id = #{id}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteRestaurant">
|
||||
DELETE FROM restaurants WHERE id = #{id}
|
||||
</delete>
|
||||
|
||||
<!-- ===== Link video-restaurant ===== -->
|
||||
|
||||
<insert id="linkVideoRestaurant">
|
||||
INSERT INTO video_restaurants (id, video_id, restaurant_id, foods_mentioned, evaluation, guests)
|
||||
VALUES (#{id}, #{videoId}, #{restaurantId}, #{foods}, #{evaluation}, #{guests})
|
||||
</insert>
|
||||
|
||||
<!-- ===== Lookups ===== -->
|
||||
|
||||
<select id="findIdByPlaceId" resultType="string">
|
||||
SELECT id FROM restaurants WHERE google_place_id = #{placeId} FETCH FIRST 1 ROWS ONLY
|
||||
</select>
|
||||
|
||||
<select id="findIdByName" resultType="string">
|
||||
SELECT id FROM restaurants WHERE name = #{name} FETCH FIRST 1 ROWS ONLY
|
||||
</select>
|
||||
|
||||
<!-- ===== Batch enrichment queries ===== -->
|
||||
|
||||
<select id="findChannelsByRestaurantIds" resultType="map">
|
||||
SELECT DISTINCT vr.restaurant_id, c.channel_name
|
||||
FROM video_restaurants vr
|
||||
JOIN videos v ON v.id = vr.video_id
|
||||
JOIN channels c ON c.id = v.channel_id
|
||||
WHERE vr.restaurant_id IN
|
||||
<foreach item="id" collection="ids" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
<select id="findFoodsByRestaurantIds" resultType="map">
|
||||
SELECT vr.restaurant_id, vr.foods_mentioned
|
||||
FROM video_restaurants vr
|
||||
WHERE vr.restaurant_id IN
|
||||
<foreach item="id" collection="ids" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
<!-- ===== Remap operations ===== -->
|
||||
|
||||
<update id="updateCuisineType">
|
||||
UPDATE restaurants SET cuisine_type = #{cuisineType} WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<update id="updateFoodsMentioned">
|
||||
UPDATE video_restaurants SET foods_mentioned = #{foods} WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<select id="findForRemapCuisine" resultType="map">
|
||||
SELECT r.id, r.name, r.cuisine_type,
|
||||
(SELECT LISTAGG(vr.foods_mentioned, '|') WITHIN GROUP (ORDER BY vr.id)
|
||||
FROM video_restaurants vr WHERE vr.restaurant_id = r.id) AS foods
|
||||
FROM restaurants r
|
||||
WHERE EXISTS (SELECT 1 FROM video_restaurants vr2 WHERE vr2.restaurant_id = r.id)
|
||||
ORDER BY r.name
|
||||
</select>
|
||||
|
||||
<select id="findForRemapFoods" resultType="map">
|
||||
SELECT vr.id, r.name, r.cuisine_type, vr.foods_mentioned, v.title
|
||||
FROM video_restaurants vr
|
||||
JOIN restaurants r ON r.id = vr.restaurant_id
|
||||
JOIN videos v ON v.id = vr.video_id
|
||||
ORDER BY r.name
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
130
backend-java/src/main/resources/mybatis/mapper/ReviewMapper.xml
Normal file
130
backend-java/src/main/resources/mybatis/mapper/ReviewMapper.xml
Normal file
@@ -0,0 +1,130 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.tasteby.mapper.ReviewMapper">
|
||||
|
||||
<resultMap id="reviewResultMap" type="com.tasteby.domain.Review">
|
||||
<id property="id" column="id"/>
|
||||
<result property="userId" column="user_id"/>
|
||||
<result property="restaurantId" column="restaurant_id"/>
|
||||
<result property="rating" column="rating"/>
|
||||
<result property="reviewText" column="review_text" typeHandler="com.tasteby.config.ClobTypeHandler"/>
|
||||
<result property="visitedAt" column="visited_at"/>
|
||||
<result property="createdAt" column="created_at"/>
|
||||
<result property="updatedAt" column="updated_at"/>
|
||||
<result property="userNickname" column="nickname"/>
|
||||
<result property="userAvatarUrl" column="avatar_url"/>
|
||||
<result property="restaurantName" column="restaurant_name"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="restaurantResultMap" type="com.tasteby.domain.Restaurant">
|
||||
<id property="id" column="id"/>
|
||||
<result property="name" column="name"/>
|
||||
<result property="address" column="address"/>
|
||||
<result property="region" column="region"/>
|
||||
<result property="latitude" column="latitude"/>
|
||||
<result property="longitude" column="longitude"/>
|
||||
<result property="cuisineType" column="cuisine_type"/>
|
||||
<result property="priceRange" column="price_range"/>
|
||||
<result property="googlePlaceId" column="google_place_id"/>
|
||||
<result property="businessStatus" column="business_status"/>
|
||||
<result property="rating" column="rating"/>
|
||||
<result property="ratingCount" column="rating_count"/>
|
||||
<result property="updatedAt" column="created_at"/>
|
||||
</resultMap>
|
||||
|
||||
<insert id="insertReview">
|
||||
INSERT INTO user_reviews (id, user_id, restaurant_id, rating, review_text, visited_at)
|
||||
VALUES (#{id}, #{userId}, #{restaurantId}, #{rating}, #{reviewText},
|
||||
<choose>
|
||||
<when test="visitedAt != null">TO_DATE(#{visitedAt}, 'YYYY-MM-DD')</when>
|
||||
<otherwise>NULL</otherwise>
|
||||
</choose>)
|
||||
</insert>
|
||||
|
||||
<update id="updateReview">
|
||||
UPDATE user_reviews SET
|
||||
rating = COALESCE(#{rating}, rating),
|
||||
review_text = COALESCE(#{reviewText}, review_text),
|
||||
visited_at = COALESCE(
|
||||
<choose>
|
||||
<when test="visitedAt != null">TO_DATE(#{visitedAt}, 'YYYY-MM-DD')</when>
|
||||
<otherwise>NULL</otherwise>
|
||||
</choose>, visited_at),
|
||||
updated_at = SYSTIMESTAMP
|
||||
WHERE id = #{id} AND user_id = #{userId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteReview">
|
||||
DELETE FROM user_reviews WHERE id = #{id} AND user_id = #{userId}
|
||||
</delete>
|
||||
|
||||
<select id="findById" resultMap="reviewResultMap">
|
||||
SELECT r.id, r.user_id, r.restaurant_id, r.rating, r.review_text,
|
||||
r.visited_at, r.created_at, r.updated_at,
|
||||
u.nickname, u.avatar_url
|
||||
FROM user_reviews r
|
||||
JOIN tasteby_users u ON u.id = r.user_id
|
||||
WHERE r.id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="findByRestaurant" resultMap="reviewResultMap">
|
||||
SELECT r.id, r.user_id, r.restaurant_id, r.rating, r.review_text,
|
||||
r.visited_at, r.created_at, r.updated_at,
|
||||
u.nickname, u.avatar_url
|
||||
FROM user_reviews r
|
||||
JOIN tasteby_users u ON u.id = r.user_id
|
||||
WHERE r.restaurant_id = #{restaurantId}
|
||||
ORDER BY r.created_at DESC
|
||||
OFFSET #{offset} ROWS FETCH NEXT #{limit} ROWS ONLY
|
||||
</select>
|
||||
|
||||
<select id="getAvgRating" resultType="map">
|
||||
SELECT ROUND(AVG(rating), 1) AS avg_rating, COUNT(*) AS review_count
|
||||
FROM user_reviews
|
||||
WHERE restaurant_id = #{restaurantId}
|
||||
</select>
|
||||
|
||||
<select id="findByUser" resultMap="reviewResultMap">
|
||||
SELECT r.id, r.user_id, r.restaurant_id, r.rating, r.review_text,
|
||||
r.visited_at, r.created_at, r.updated_at,
|
||||
u.nickname, u.avatar_url,
|
||||
rest.name AS restaurant_name
|
||||
FROM user_reviews r
|
||||
JOIN tasteby_users u ON u.id = r.user_id
|
||||
LEFT JOIN restaurants rest ON rest.id = r.restaurant_id
|
||||
WHERE r.user_id = #{userId}
|
||||
ORDER BY r.created_at DESC
|
||||
OFFSET #{offset} ROWS FETCH NEXT #{limit} ROWS ONLY
|
||||
</select>
|
||||
|
||||
<select id="countFavorite" resultType="int">
|
||||
SELECT COUNT(*) FROM user_favorites
|
||||
WHERE user_id = #{userId} AND restaurant_id = #{restaurantId}
|
||||
</select>
|
||||
|
||||
<insert id="insertFavorite">
|
||||
INSERT INTO user_favorites (id, user_id, restaurant_id)
|
||||
VALUES (#{id}, #{userId}, #{restaurantId})
|
||||
</insert>
|
||||
|
||||
<delete id="deleteFavorite">
|
||||
DELETE FROM user_favorites
|
||||
WHERE user_id = #{userId} AND restaurant_id = #{restaurantId}
|
||||
</delete>
|
||||
|
||||
<select id="findFavoriteId" resultType="string">
|
||||
SELECT id FROM user_favorites
|
||||
WHERE user_id = #{userId} AND restaurant_id = #{restaurantId}
|
||||
</select>
|
||||
|
||||
<select id="getUserFavorites" resultMap="restaurantResultMap">
|
||||
SELECT r.id, r.name, r.address, r.region, r.latitude, r.longitude,
|
||||
r.cuisine_type, r.price_range, r.google_place_id,
|
||||
r.business_status, r.rating, r.rating_count, f.created_at
|
||||
FROM user_favorites f
|
||||
JOIN restaurants r ON r.id = f.restaurant_id
|
||||
WHERE f.user_id = #{userId}
|
||||
ORDER BY f.created_at DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.tasteby.mapper.SearchMapper">
|
||||
|
||||
<resultMap id="restaurantMap" type="Restaurant">
|
||||
<id property="id" column="id"/>
|
||||
<result property="name" column="name"/>
|
||||
<result property="address" column="address"/>
|
||||
<result property="region" column="region"/>
|
||||
<result property="latitude" column="latitude"/>
|
||||
<result property="longitude" column="longitude"/>
|
||||
<result property="cuisineType" column="cuisine_type"/>
|
||||
<result property="priceRange" column="price_range"/>
|
||||
<result property="googlePlaceId" column="google_place_id"/>
|
||||
<result property="businessStatus" column="business_status"/>
|
||||
<result property="rating" column="rating"/>
|
||||
<result property="ratingCount" column="rating_count"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="keywordSearch" resultMap="restaurantMap">
|
||||
SELECT DISTINCT r.id, r.name, r.address, r.region, r.latitude, r.longitude,
|
||||
r.cuisine_type, r.price_range, r.google_place_id,
|
||||
r.business_status, r.rating, r.rating_count
|
||||
FROM restaurants r
|
||||
JOIN video_restaurants vr ON vr.restaurant_id = r.id
|
||||
JOIN videos v ON v.id = vr.video_id
|
||||
WHERE r.latitude IS NOT NULL
|
||||
AND (UPPER(r.name) LIKE UPPER(#{query})
|
||||
OR UPPER(r.address) LIKE UPPER(#{query})
|
||||
OR UPPER(r.region) LIKE UPPER(#{query})
|
||||
OR UPPER(r.cuisine_type) LIKE UPPER(#{query})
|
||||
OR UPPER(vr.foods_mentioned) LIKE UPPER(#{query})
|
||||
OR UPPER(v.title) LIKE UPPER(#{query}))
|
||||
FETCH FIRST #{limit} ROWS ONLY
|
||||
</select>
|
||||
|
||||
<select id="findChannelsByRestaurantIds" resultType="map">
|
||||
SELECT DISTINCT vr.restaurant_id, c.channel_name
|
||||
FROM video_restaurants vr
|
||||
JOIN videos v ON v.id = vr.video_id
|
||||
JOIN channels c ON c.id = v.channel_id
|
||||
WHERE vr.restaurant_id IN
|
||||
<foreach item="id" collection="ids" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.tasteby.mapper.StatsMapper">
|
||||
|
||||
<update id="recordVisit">
|
||||
MERGE INTO site_visits sv
|
||||
USING (SELECT TRUNC(SYSDATE) AS d FROM dual) src
|
||||
ON (sv.visit_date = src.d)
|
||||
WHEN MATCHED THEN UPDATE SET sv.visit_count = sv.visit_count + 1
|
||||
WHEN NOT MATCHED THEN INSERT (visit_date, visit_count) VALUES (src.d, 1)
|
||||
</update>
|
||||
|
||||
<select id="getTodayVisits" resultType="int">
|
||||
SELECT NVL(visit_count, 0)
|
||||
FROM site_visits
|
||||
WHERE visit_date = TRUNC(SYSDATE)
|
||||
</select>
|
||||
|
||||
<select id="getTotalVisits" resultType="int">
|
||||
SELECT NVL(SUM(visit_count), 0)
|
||||
FROM site_visits
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.tasteby.mapper.UserMapper">
|
||||
|
||||
<resultMap id="userResultMap" type="com.tasteby.domain.UserInfo">
|
||||
<id property="id" column="id"/>
|
||||
<result property="email" column="email"/>
|
||||
<result property="nickname" column="nickname"/>
|
||||
<result property="avatarUrl" column="avatar_url"/>
|
||||
<result property="isAdmin" column="is_admin" javaType="boolean"/>
|
||||
<result property="provider" column="provider"/>
|
||||
<result property="createdAt" column="created_at"/>
|
||||
<result property="favoriteCount" column="favorite_count"/>
|
||||
<result property="reviewCount" column="review_count"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="findByProviderAndProviderId" resultMap="userResultMap">
|
||||
SELECT id, email, nickname, avatar_url, is_admin, provider, created_at
|
||||
FROM tasteby_users
|
||||
WHERE provider = #{provider} AND provider_id = #{providerId}
|
||||
</select>
|
||||
|
||||
<update id="updateLastLogin">
|
||||
UPDATE tasteby_users SET last_login_at = SYSTIMESTAMP WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<insert id="insert">
|
||||
INSERT INTO tasteby_users (id, provider, provider_id, email, nickname, avatar_url)
|
||||
VALUES (#{id}, #{provider}, #{providerId}, #{email}, #{nickname}, #{avatarUrl})
|
||||
</insert>
|
||||
|
||||
<select id="findById" resultMap="userResultMap">
|
||||
SELECT id, email, nickname, avatar_url, is_admin, provider, created_at
|
||||
FROM tasteby_users
|
||||
WHERE id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="findAllWithCounts" resultMap="userResultMap">
|
||||
SELECT u.id, u.email, u.nickname, u.avatar_url, u.provider, u.created_at,
|
||||
NVL(fav.cnt, 0) AS favorite_count,
|
||||
NVL(rev.cnt, 0) AS review_count
|
||||
FROM tasteby_users u
|
||||
LEFT JOIN (SELECT user_id, COUNT(*) AS cnt FROM user_favorites GROUP BY user_id) fav ON fav.user_id = u.id
|
||||
LEFT JOIN (SELECT user_id, COUNT(*) AS cnt FROM user_reviews GROUP BY user_id) rev ON rev.user_id = u.id
|
||||
ORDER BY u.created_at DESC
|
||||
OFFSET #{offset} ROWS FETCH NEXT #{limit} ROWS ONLY
|
||||
</select>
|
||||
|
||||
<select id="countAll" resultType="int">
|
||||
SELECT COUNT(*) FROM tasteby_users
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.tasteby.mapper.VectorMapper">
|
||||
|
||||
<resultMap id="vectorSearchResultMap" type="VectorSearchResult">
|
||||
<result property="restaurantId" column="restaurant_id"/>
|
||||
<result property="chunkText" column="chunk_text"
|
||||
typeHandler="com.tasteby.config.ClobTypeHandler"/>
|
||||
<result property="distance" column="dist"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="searchSimilar" resultMap="vectorSearchResultMap">
|
||||
<![CDATA[
|
||||
SELECT rv.restaurant_id, rv.chunk_text,
|
||||
VECTOR_DISTANCE(rv.embedding, TO_VECTOR(#{queryVec}), COSINE) AS dist
|
||||
FROM restaurant_vectors rv
|
||||
WHERE VECTOR_DISTANCE(rv.embedding, TO_VECTOR(#{queryVec}), COSINE) <= #{maxDistance}
|
||||
ORDER BY dist
|
||||
FETCH FIRST #{topK} ROWS ONLY
|
||||
]]>
|
||||
</select>
|
||||
|
||||
<insert id="insertVector">
|
||||
INSERT INTO restaurant_vectors (id, restaurant_id, chunk_text, embedding)
|
||||
VALUES (#{id}, #{restaurantId}, #{chunkText}, TO_VECTOR(#{embedding}))
|
||||
</insert>
|
||||
|
||||
</mapper>
|
||||
219
backend-java/src/main/resources/mybatis/mapper/VideoMapper.xml
Normal file
219
backend-java/src/main/resources/mybatis/mapper/VideoMapper.xml
Normal file
@@ -0,0 +1,219 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.tasteby.mapper.VideoMapper">
|
||||
|
||||
<!-- ===== Result Maps ===== -->
|
||||
|
||||
<resultMap id="videoSummaryMap" type="VideoSummary">
|
||||
<id property="id" column="id"/>
|
||||
<result property="videoId" column="video_id"/>
|
||||
<result property="title" column="title"/>
|
||||
<result property="url" column="url"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="publishedAt" column="published_at"/>
|
||||
<result property="channelName" column="channel_name"/>
|
||||
<result property="hasTranscript" column="has_transcript"/>
|
||||
<result property="hasLlm" column="has_llm"/>
|
||||
<result property="restaurantCount" column="restaurant_count"/>
|
||||
<result property="matchedCount" column="matched_count"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="videoDetailMap" type="VideoDetail">
|
||||
<id property="id" column="id"/>
|
||||
<result property="videoId" column="video_id"/>
|
||||
<result property="title" column="title"/>
|
||||
<result property="url" column="url"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="publishedAt" column="published_at"/>
|
||||
<result property="channelName" column="channel_name"/>
|
||||
<result property="transcriptText" column="transcript_text"
|
||||
typeHandler="com.tasteby.config.ClobTypeHandler"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="videoRestaurantLinkMap" type="VideoRestaurantLink">
|
||||
<result property="restaurantId" column="id"/>
|
||||
<result property="name" column="name"/>
|
||||
<result property="address" column="address"/>
|
||||
<result property="cuisineType" column="cuisine_type"/>
|
||||
<result property="priceRange" column="price_range"/>
|
||||
<result property="region" column="region"/>
|
||||
<result property="foodsMentioned" column="foods_mentioned"
|
||||
typeHandler="com.tasteby.config.ClobTypeHandler"/>
|
||||
<result property="evaluation" column="evaluation"
|
||||
typeHandler="com.tasteby.config.ClobTypeHandler"/>
|
||||
<result property="guests" column="guests"
|
||||
typeHandler="com.tasteby.config.ClobTypeHandler"/>
|
||||
<result property="googlePlaceId" column="google_place_id"/>
|
||||
<result property="latitude" column="latitude"/>
|
||||
<result property="longitude" column="longitude"/>
|
||||
</resultMap>
|
||||
|
||||
<!-- ===== Queries ===== -->
|
||||
|
||||
<select id="findAll" resultMap="videoSummaryMap">
|
||||
SELECT v.id, v.video_id, v.title, v.url, v.status,
|
||||
v.published_at, c.channel_name,
|
||||
CASE WHEN v.transcript_text IS NOT NULL AND dbms_lob.getlength(v.transcript_text) > 0 THEN 1 ELSE 0 END AS has_transcript,
|
||||
CASE WHEN v.llm_raw_response IS NOT NULL AND dbms_lob.getlength(v.llm_raw_response) > 0 THEN 1 ELSE 0 END AS has_llm,
|
||||
(SELECT COUNT(*) FROM video_restaurants vr WHERE vr.video_id = v.id) AS restaurant_count,
|
||||
(SELECT COUNT(*) FROM video_restaurants vr JOIN restaurants r ON r.id = vr.restaurant_id
|
||||
WHERE vr.video_id = v.id AND r.google_place_id IS NOT NULL) AS matched_count
|
||||
FROM videos v
|
||||
JOIN channels c ON c.id = v.channel_id
|
||||
<if test="status != null and status != ''">
|
||||
WHERE v.status = #{status}
|
||||
</if>
|
||||
ORDER BY v.published_at DESC NULLS LAST
|
||||
</select>
|
||||
|
||||
<select id="findDetail" resultMap="videoDetailMap">
|
||||
SELECT v.id, v.video_id, v.title, v.url, v.status,
|
||||
v.published_at, v.transcript_text, c.channel_name
|
||||
FROM videos v
|
||||
JOIN channels c ON c.id = v.channel_id
|
||||
WHERE v.id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="findVideoRestaurants" resultMap="videoRestaurantLinkMap">
|
||||
SELECT r.id, r.name, r.address, r.cuisine_type, r.price_range, r.region,
|
||||
vr.foods_mentioned, vr.evaluation, vr.guests,
|
||||
r.google_place_id, r.latitude, r.longitude
|
||||
FROM video_restaurants vr
|
||||
JOIN restaurants r ON r.id = vr.restaurant_id
|
||||
WHERE vr.video_id = #{videoId}
|
||||
</select>
|
||||
|
||||
<!-- ===== Updates ===== -->
|
||||
|
||||
<update id="updateStatus">
|
||||
UPDATE videos SET status = #{status} WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<update id="updateTitle">
|
||||
UPDATE videos SET title = #{title} WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<update id="updateTranscript">
|
||||
UPDATE videos SET transcript_text = #{transcript} WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<update id="updateVideoFields">
|
||||
UPDATE videos SET
|
||||
status = #{status},
|
||||
processed_at = SYSTIMESTAMP
|
||||
<if test="transcript != null">
|
||||
, transcript_text = #{transcript}
|
||||
</if>
|
||||
<if test="llmResponse != null">
|
||||
, llm_raw_response = #{llmResponse}
|
||||
</if>
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<!-- ===== Cascade deletes for video deletion ===== -->
|
||||
|
||||
<delete id="deleteVectorsByVideoOnly">
|
||||
DELETE FROM restaurant_vectors WHERE restaurant_id IN (
|
||||
SELECT vr.restaurant_id FROM video_restaurants vr
|
||||
WHERE vr.video_id = #{videoId}
|
||||
AND NOT EXISTS (SELECT 1 FROM video_restaurants vr2
|
||||
WHERE vr2.restaurant_id = vr.restaurant_id AND vr2.video_id != #{videoId})
|
||||
)
|
||||
</delete>
|
||||
|
||||
<delete id="deleteReviewsByVideoOnly">
|
||||
DELETE FROM user_reviews WHERE restaurant_id IN (
|
||||
SELECT vr.restaurant_id FROM video_restaurants vr
|
||||
WHERE vr.video_id = #{videoId}
|
||||
AND NOT EXISTS (SELECT 1 FROM video_restaurants vr2
|
||||
WHERE vr2.restaurant_id = vr.restaurant_id AND vr2.video_id != #{videoId})
|
||||
)
|
||||
</delete>
|
||||
|
||||
<delete id="deleteFavoritesByVideoOnly">
|
||||
DELETE FROM user_favorites WHERE restaurant_id IN (
|
||||
SELECT vr.restaurant_id FROM video_restaurants vr
|
||||
WHERE vr.video_id = #{videoId}
|
||||
AND NOT EXISTS (SELECT 1 FROM video_restaurants vr2
|
||||
WHERE vr2.restaurant_id = vr.restaurant_id AND vr2.video_id != #{videoId})
|
||||
)
|
||||
</delete>
|
||||
|
||||
<delete id="deleteRestaurantsByVideoOnly">
|
||||
DELETE FROM restaurants WHERE id IN (
|
||||
SELECT vr.restaurant_id FROM video_restaurants vr
|
||||
WHERE vr.video_id = #{videoId}
|
||||
AND NOT EXISTS (SELECT 1 FROM video_restaurants vr2
|
||||
WHERE vr2.restaurant_id = vr.restaurant_id AND vr2.video_id != #{videoId})
|
||||
)
|
||||
</delete>
|
||||
|
||||
<delete id="deleteVideoRestaurants">
|
||||
DELETE FROM video_restaurants WHERE video_id = #{videoId}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteVideo">
|
||||
DELETE FROM videos WHERE id = #{videoId}
|
||||
</delete>
|
||||
|
||||
<!-- ===== Single video-restaurant unlink + orphan cleanup ===== -->
|
||||
|
||||
<delete id="deleteOneVideoRestaurant">
|
||||
DELETE FROM video_restaurants WHERE video_id = #{videoId} AND restaurant_id = #{restaurantId}
|
||||
</delete>
|
||||
|
||||
<delete id="cleanupOrphanVectors">
|
||||
DELETE FROM restaurant_vectors WHERE restaurant_id = #{restaurantId}
|
||||
AND NOT EXISTS (SELECT 1 FROM video_restaurants WHERE restaurant_id = #{restaurantId})
|
||||
</delete>
|
||||
|
||||
<delete id="cleanupOrphanReviews">
|
||||
DELETE FROM user_reviews WHERE restaurant_id = #{restaurantId}
|
||||
AND NOT EXISTS (SELECT 1 FROM video_restaurants WHERE restaurant_id = #{restaurantId})
|
||||
</delete>
|
||||
|
||||
<delete id="cleanupOrphanFavorites">
|
||||
DELETE FROM user_favorites WHERE restaurant_id = #{restaurantId}
|
||||
AND NOT EXISTS (SELECT 1 FROM video_restaurants WHERE restaurant_id = #{restaurantId})
|
||||
</delete>
|
||||
|
||||
<delete id="cleanupOrphanRestaurant">
|
||||
DELETE FROM restaurants WHERE id = #{restaurantId}
|
||||
AND NOT EXISTS (SELECT 1 FROM video_restaurants WHERE restaurant_id = #{restaurantId})
|
||||
</delete>
|
||||
|
||||
<!-- ===== Insert / Lookup ===== -->
|
||||
|
||||
<insert id="insertVideo">
|
||||
INSERT INTO videos (id, channel_id, video_id, title, url, published_at)
|
||||
VALUES (#{id}, #{channelId}, #{videoId}, #{title}, #{url}, #{publishedAt})
|
||||
</insert>
|
||||
|
||||
<select id="getExistingVideoIds" resultType="string">
|
||||
SELECT video_id FROM videos WHERE channel_id = #{channelId}
|
||||
</select>
|
||||
|
||||
<select id="getLatestVideoDate" resultType="string">
|
||||
SELECT TO_CHAR(MAX(published_at), 'YYYY-MM-DD"T"HH24:MI:SS"Z"')
|
||||
FROM videos WHERE channel_id = #{channelId}
|
||||
</select>
|
||||
|
||||
<!-- ===== Pipeline queries ===== -->
|
||||
|
||||
<select id="findPendingVideos" resultType="map">
|
||||
SELECT id, video_id, title, url FROM videos
|
||||
WHERE status = 'pending' ORDER BY created_at
|
||||
FETCH FIRST #{limit} ROWS ONLY
|
||||
</select>
|
||||
|
||||
<select id="findVideosForBulkExtract" resultType="map">
|
||||
SELECT v.id, v.video_id, v.title, v.url, v.transcript_text
|
||||
FROM videos v
|
||||
WHERE v.transcript_text IS NOT NULL
|
||||
AND dbms_lob.getlength(v.transcript_text) > 0
|
||||
AND (v.llm_raw_response IS NULL OR dbms_lob.getlength(v.llm_raw_response) = 0)
|
||||
AND v.status != 'skip'
|
||||
ORDER BY v.published_at DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
|
||||
<configuration>
|
||||
<settings>
|
||||
<setting name="mapUnderscoreToCamelCase" value="true"/>
|
||||
<setting name="callSettersOnNulls" value="true"/>
|
||||
<setting name="returnInstanceForEmptyRow" value="true"/>
|
||||
</settings>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user