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:
joungmin
2026-03-09 21:13:44 +09:00
parent 91d0ad4598
commit c16add08c3
63 changed files with 2155 additions and 1483 deletions

View File

@@ -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>