Spring으로 쿼리를 만들다보면 여러개의 테이블을 조인한 쿼리에서 다른 테이블 칼럼의 값까지 읽어올 필요가 있다. 예를 들면 글정보를 받아 오는 api가 있는데 내가 그 글을 좋아요 했는지, 안했는지 유무까지 알려주는 요구 사항의 경우 두 개의 테이블을 조인해야한다.

 

쿼리문을 짜면 다음과 같다. tb_post에 있는 모든 필드를 가져오고, 좋아요 유무는 liked 필드 이름으로 받아오는 것으로 뒀다.

select tb_post.*, when tb_post_user_like.post_id > 0 then true else false end as liked from tb_post
left join tb_post_like on (tb_post.post_id = tb_post_user_like.post_id and tb_post_like.user_id = :findUserId)
where tb_post.post_id = :postId

 

JPA에서 제공하는 CrudRepository 로는 이미 있는 테이블의 칼럼을 매핑해서 받아오는데 반해 이 방법은 임의로 liked라는 새 칼럼을 생성한 것이기 때문에 테이블과 1:1 매핑된 엔티티 클래스에서 했던 것처럼 칼럼을 자동으로 매핑하는게 안되고 다른 방법을 써야한다. 열심히 구글 검색을 해본 결과 세가지 방법을 발견했다. 

 

1. Object형태로 받아오기 

Query문에서 리턴 값을 Object로 받아오면 모든 필드 값의 리턴을 받아 올 수 있다. 가장 직관적이고 쉬운 방법이다.

 

@Query(value = "select tb_post.*, case when tb_post_like.post_id > 0 then true else false end as liked from tb_post \n" +
        "from tb_post \n" +
        "left join tb_post_like on (tb_post.post_id = tb_post_like.post_id and tb_post_like.user_id = :findUserId)\n" +
        "where tb_post.post_id = :postId", nativeQuery = true)
fun findPostById(@Param("postId") postId: Long, @Param("findUserId") findUserId: Long) : List<Array<Any>>

하지만 이렇게 읽어오면 아래 그림처럼 필드 값이 생략돼서 날라오게 돼서 알아보기가 힘들다. 쿼리문에서 칼럼 필드 순서를 지정하는 방법으로 처리할 수 있으나 알아보기가 힘들어서 관리하기가 어려운 단점이 있어 추천하지 않는다. 위와 같은 형태로 읽어오는 클래스를 만든다면 더더욱 쓰지 않는게 좋다.

 

 

2. JPA New 명령어 

 

JPA 쿼리의 New 명령어를 사용하면 리턴 값을 클래스로 줄 수 있다. 단 이 방법은 native query 문을 사용하지 못하고 jpa 쿼리를 사용해야 한다는 점이다. limit을 쓰는 구문에서는 사용할 수 없다.

 

data class Post(postId: Long, postTitle: String, postContent: String, liked:Boolean)

@Query(value = "select new com.package.Post(tb_post.post_id, tb_post.post_title, tb_post.post_content, case when tb_post_like.post_id > 0 then true else false end) from tb_post from tb_post left join tb_post_like on (tb_post.post_id = tb_post_like.post_id and tb_post_like.user_id = :findUserId) where tb_post.post_id = :postId")
fun findPostById(@Param("postId") postId: Long, @Param("findUserId") findUserId: Long) : List<Post>

 

3. SetQueryResultSetMapping 

 

쿼리에서 읽어온 컬럼 필드를 클래스에 매핑 해줄 수 있는 어노테이션이다. ConstructorResult 어노테이션에서 칼럼 필드 값을 읽어와서 Post 클래스의 생성자로 만들 수 있다. 하단의 NamedNativeQuery 어노테이션에서는 쿼리의 이름을 정하고, ConstructorResult에서 참조하는 필드의 형태로 읽어 올 수 있도록 Select 문을 만들어 주고 사용할 mapping을 SqlResultSetMapping에서 지정한 이름과 동일한 값을 입력한다. 이렇게 하면 이 쿼리는 자동으로 Post 클래스 값을 출력하는 쿼리가 된다.

 

@SqlResultSetMapping(
        name = "PostMapping",
        classes = [
                ConstructorResult(
                        targetClass = Post::class,
                        columns = [
                                ColumnResult(name = "post_id", type = Long::class),
                                ColumnResult(name = "post_title", type = String::class),
                                ColumnResult(name = "post_content", type = String::class),
                                ColumnResult(name = "liked", type = Boolean::class)
                        ]
                )
        ]
)
@NamedNativeQueries(value = [
    NamedNativeQuery(name = "findPostByIdBaseOnUser", query = "select tb_post.post_id, tb_post.post_title, tb_post.post_content, case when tb_post_like.post_id > 0 then true else false end as liked from tb_post \n" +
            "left join tb_post_like on (tb_post.post_id = tb_post_like.post_id and tb_post_like.user_id = :findUserId)\n" +
            "where tb_post.post_id = :postId resultSetMapping = "PostMapping")
])
@Entity
data class Post(
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        val postId: Long,
        @Column
        val postTitle: String,
        @Column
        val postContent: String,
        @Column
        val liked: Boolean = false
): Serializable

 

아래의 코드로 쿼리 호출이 가능하다. 앞서 설저한 NamedNativeQuery의 이름 값과 동일하게 넣는다. 초반에 보일러플레이트 코드가 많지만 Post 클래스를 지속적으로 사용하고자 한다면 필드 값을 참조 할 수 있기 때문에 관리가 더 편리하다. 

 

@Service
class PostService {
    @PersistenceContext
    lateinit var entityManager: EntityManager

    fun postById(postId: Long, findUserId: Long): Post? {
        return entityManager.createNamedQuery("findPostByIdBaseOnUser", Post::class.java)
                .setParameter("postId", postId)
                .setParameter("findUserId", findUserId)
                .resultList
                .firstOrNull()

    }
}
728x90

'개발 > spring' 카테고리의 다른 글

Node.js vs Spring Boot  (3) 2021.03.13
Spring 테이블 칼럼이 아닌 필드 데이터 받아오기  (0) 2021.03.05

RoundedFrameLayout

개발/안드로이드 2021. 3. 3. 13:26 Posted by 아는 개발자

레딧의 투데이 피드탭

디자이너와 협업하다보면 위 그림처럼 이미지의 꼭지점 부분에 radius를 넣어야하는 경우가 종종 생긴다. 아이콘으로 넣는 이미지의 경우에는 디자이너가 직접 아이콘의 radius를 먹일 수 있는데 뉴스피드처럼 외부에서 받아오는 이미지의 경우에는 매번 작업을 할 수 없어 코딩으로 처리해야한다. 이럴 경우 RoundedFrameLayout 라이브러리를 사용하면 쉽게 처리가 가능하다.

 

1. 라이브러리 설치 

 

build.gradle에 추가해서 적용한다.

 

dependencies {
    // RoundedFrameLayout
    compile 'com.github.QuarkWorks:RoundedFrameLayout-Android:0.3.7'
}

 

2. 적용 

 

RoundedFrameLayout은 이미지가 적용되는 ImageView의 부모로 설정한다. 뷰의 속성 값으로 cornerRadius가 있는데 이 값을 이용해서 얼마나 깎을 것인지 적용 할 수 있다. ImageView는 부모 레이아웃이 변경됐으므로 자동으로 적용되게된다.

 

<com.quarkworks.roundedframelayout.RoundedFrameLayout
    android:id="@+id/layout_rounded_image"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:cornerRadiusTopLeft="10dp"
    app:cornerRadiusTopRight="10dp"
    app:cornerRadiusBottomLeft="10dp"
    app:cornerRadiusBottomRight="10dp">
    <ImageView
        android:id="@+id/layout_rounded_image_iv"
        android:background="#0c000000"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"/>
</com.quarkworks.roundedframelayout.RoundedFrameLayout>
728x90

'개발 > 안드로이드' 카테고리의 다른 글

jitpack 이란  (0) 2021.03.28
안드로이드 스튜디오를 이용한 네트워크 디버깅  (0) 2021.03.14
RoundedFrameLayout  (0) 2021.03.03
겹치는 recyclerview 만들기  (0) 2021.02.15
android - Hilt 사용기  (0) 2021.01.15
item decoration  (0) 2020.12.06

겹치는 recyclerview 만들기

개발/안드로이드 2021. 2. 15. 17:44 Posted by 아는 개발자

서비스 개발 하다 보면 위 그림처럼 recyclerview인데 아이템을 겹치는 형태로 만들어야 할 때가 있다. 먼저 쉽게 생각해 볼 수 있는 방법은 ItemDecoration을 이용해 item1을 제외한 item2, item3의 left 오프셋을 왼쪽으로 당겨주는 방법이 있다.

 

rv.addItemDecoration(object: RecyclerView.ItemDecoration() {
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        val position = parent.getChildAdapterPosition(view)
        if (position != 0) outRect.left = DimensionUtils.dp2px(requireContext(), 10f).toInt() * -1  
    }
})

 

그런데 이렇게 만들면 예상했던 것과 다르게 뒤에 있는 아이템이 앞에 있던 아이템 위로 올라가게 된다. 뒤에 있는 아이템을 우선순위를 높게 쳐서 발생하는 에러다.

 

 

처음에 계획했던 대로 만들려면 recyclerview 에 약간 트릭을 추가해야한다. 사용한 LinearLayoutManager에서 reverseLayout과 stackFronEnd 속성 값을 true로 설정한다. reverLayout을 true로 두면 아이템을 RTL에 맞춰서 오른쪽으로 쌓는 것이고, stackFronEnd는 recyclerview 영역의 끝부분부터 채우는 것이다. item을 역순으로 출력할 것이므로, 맨 앞에 있는 것은 맨 뒤로 가기 때문에 offset 설정 함수도 끝 부분이 이동하도록 바꿔준다.

 

rv.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false).apply {
    reverseLayout = true
    stackFromEnd = true
}

rv.addItemDecoration(object: RecyclerView.ItemDecoration() {
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        val position = parent.getChildAdapterPosition(view)
        if (position != (adapter?.itemCount?: 0) - 1) {
            outRect.left = DimensionUtils.dp2px(context, 10f).toInt() * -1
        }
    }
})

 

 

위 코드로 설정하면 아래와 같은 그림이 나온다. 예상했던 그림이긴 한데, item 순서가 역순이다.  해결방법은 간단하다. rv의 adapter에 item을 넣을 때 역순으로 넣으면 된다.

 

adapter?.submitItems(it.reversed())

 

결과 이렇게 겹치는 recyclerview 아이템을 볼 수 있다.

 

728x90

git 에 올라간 파일 이름 확인하기

개발/삽질 기록 2021. 1. 26. 20:07 Posted by 아는 개발자

이전 포스트에서 맥에서 파일의 대소문자 구분을 하지 않는 특성 때문에 크게 한번 삽질한 적이 있었는데 얼마 지나지 않아서 같은 삽질을 반복하고 말았다... 내 로컬 저장소에서는 아무 문제 없이 돌아가는데 리눅스 기반 heroku에서는 파일을 찾지 못하는 에러를 또 보고 말았다.. 부글부글.

 

이번에는 깃허브에서 관리하는 프로젝트도 아니라서 무슨 파일이 잘못됐는지 찾기도 어려웠는데 다행히 명령어 한줄로 긴 삽질을 막을 수 있었다. 깃에 올라간 파일 이름을 리스트로 출력할 수 있다.

 

user@kwony mytrot-admin % git ls-files
.env.development
.env.production
.gitignore
README.md
package.json
public/favicon.ico
public/index.html
public/logo192.png
public/logo512.png
728x90

android - Hilt 사용기

개발/안드로이드 2021. 1. 15. 14:29 Posted by 아는 개발자

예전에 쓴 Hilt 포스트에선 기존에 사용중인 프로젝트에 Hilt를 쉽게 적용할 수 없어 아쉽다는 점을 다루었다. 그래서 최근에 소소하게 시작한 사이드프로젝트에선 처음부터 Hilt를 도입해서 사용해봤다. 확실히 Dagger에 비해 자유롭고 사용하기가 간편했다. 이번 포스트에서는 어떤점이 좋았는지를 다뤄보고자 한다. 

 

1. private val 변수 형태로 주입 가능.

 

Dagger로 의존성을 주입할 때는 @Inject 어노테이션과 뒤에 lateinit var 을 붙여줘야했다. 그런데 앞으로 바뀌지 않을 변수에 var 형태로 선언하는게 여간 찝찝한게 아니었다. 다행히 Hilt에서는 이런 찝찝함을 해결했다. 생성자의 인자로 추가해 의존성을 주입할 수 있어 값이 변경되지 않은 val 형태로 주입이 가능하다. 아래 코드는 @ViewModelInject 어노테이션을 이용해 module에서 선언된 객체들에 바로 의존성을 주입하는 코드다. private 변수로도 주입이 가능하다.

 

class AssetEditorViewModel @ViewModelInject constructor(
    @Assisted private val savedStateHandle: SavedStateHandle,
    application: Application,
    private val assetRepository: AssetRepository,
    private val assetTypeRepository: AssetTypeRepository
): AndroidViewModel(application) {

}

@Module
@InstallIn(ApplicationComponent::class)
class DatabaseModule {
    ...

    @Singleton
    @Provides
    fun provideAssetRepository(appDatabase: AppDatabase) = AssetRepository(appDatabase.assetDao())

    @Singleton
    @Provides
    fun provideAssetTypeRepository(appDatabase: AppDatabase) = AssetTypeRepository(appDatabase.assetTypeDao())
}

 

물론 activity, fragment 처럼 생성자를 customize 할 수 없는 클래스도 있다. 이런 경우 기존과 동일하게 lateinit var를 붙인 채로 주입이 가능하다.

 

@AndroidEntryPoint
class MainFragment : BaseFragment(R.layout.fragment_main) {

    @Inject lateinit var assetRepository: AssetRepository

 

2. ViewModel 의존성 주입이 쉽다

 

Dagger에서는 ViewModel 을 공식적으로 지원해주는게 아니어서 별도의 Factory 클래스를 만들어서 주입을 해줘야 했다. 예로 Fragment를 만들면 이 Fragment Module에선 주입할 ViewModel을 팩토리 형태로 만들어줘야하고 ViewModelMap에 따로 등록도 해줘야하고 결과적으로 코드가 너무 늘어나 관리가 어렵다. Hilt에서는 ViewModel 의존성 주입을 공식적으로 지원해주기 시작했다.

 

ViewModel은 @ViewModelInject 어노테이션을 생성자 앞에 붙이고 ViewModel에서 사용하려는 의존성 주입 클래스를 선언만 하면 된다. Activity, Fragment 단에서는 코틀린 delegate 속성인 by viewModels(), by activityViewModels()를 통해 ViewModel을 받으면 평소와 동일하게 사용할 수 있다.

 

@AndroidEntryPoint
class MainActivity : BaseActivity() {
    private val mainViewModel: MainViewModel by viewModels()
}

@AndroidEntryPoint
class AssetsFragment: Fragment(R.layout.fragment_assets) {
    private val mainViewModel: MainViewModel by activityViewModels()
}

class MainViewModel @ViewModelInject constructor(
    @Assisted private val savedStateHandle: SavedStateHandle,
    application: Application,
    private val accountRepository: AccountRepository,

 

3. Module 만들고 등록 할 필요가 없다.

 

Dagger에서는 어떤 Module을 만들면 Dagger에 등록해주는 Module에다가 추가해야했다. 그래서 열심히 Module을 만들어도 추가하는 작업을 빼먹어으면 런타임시 에러가 수두룩 뜨곤 했었다. 근데 Hilt에서는 따로 추가하는 작업 없이 @InstallIn 어노테이션만 추가해주면 된다. 귀찮고 빼먹기 쉬운 코드를 확 줄일 수 있었다.

 

@Module
@InstallIn(ApplicationComponent::class)
class DatabaseModule {
    @Singleton
    @Provides
    fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(context, AppDatabase::class.java, "database")
            .build()
    }

 

이외에도 편리한 점이 더 많을텐데 사이드 프로젝트 규모가 크지 않아서 아직 다 경험하지 못한 것 같다... 앞으로 쓰다가 괜찮으면 추가로 정리해서 올려야지.

728x90