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:
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>
|
||||
Reference in New Issue
Block a user