remagine

혼자서 웹 서비스 만들어보기 - 7 본문

개인프로젝트 - 웹소설 사이트

혼자서 웹 서비스 만들어보기 - 7

remagine 2017. 6. 30. 16:50

혼자서 웹 서비스 만들어보기 - 7

https://github.com/remagine/webNovel/tree/develop




1. 태그 검색 만들기


 게시물에는 Tag라는 컬럼이 있습니다. 해당 컬럼은 현재 String(Varchar) 타입 컬럼입니다.


 보통 Tag로 검색을 할 때 어떻게 구현할지 생각해 봤습니다.


 apink라는 단어로 검색을 한다고 해보겠습니다.

 


만일 Tag가 String 컬럼이라면


 "apink, sistar, twice, exo" 이런 식으로 저장이 되어있을 것 입니다.


검색을 구현한다면


 select * from story where tag ilike "%apink%"


apink를 검색하기 위해선 like 검색에 %를 앞뒤로 붙여줘야 합니다. 


이렇게 검색하면 일단 index는 태울 수 없기에 사이트가 커지면 속도가 엄청 느리게 될 것 입니다.



현재 Postgresql은 Array 컬럼을 지원하고 있습니다.


그리고 Gin index라는 index와 


range 검색어인 @> 연산자를 지원합니다.


참조 :: https://www.postgresql.org/docs/9.3/static/functions-range.html


이렇게 되면


"apink, sistar, twice, exo" 는


[apink,sister,twice,exo] 형태로 저장이 됩니다.


index를 태울 수 있는 장점이 있지만


apink를 검색하려면 apink로 검색해야지 apin을 like검색으로 할 수는 없습니다.


현재 String(Varchar)타입인 Tag컬럼을 String[]로 변경해 보려고 합니다.



기존 tag 컬럼의 type을 변경하는 것은 불가능함으로


tags라는 컬럼을 character varying[] length 300으로 잡고 생성합니다.



 2. Entity 변경


tag 컬럼을 tags컬럼으로 바꿔줘야 겠네요.


일반적인 JPA는 array형식의 컬럼을 받지 않습니다. 


 
import java.io.Serializable;
import java.sql.Array;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
 
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.usertype.UserType;
 
public class StringArrayType implements UserType {
 
    private final int[] arrayTypes = new int[] { Types.ARRAY };
 
    @Override
    public int[] sqlTypes() {
        return arrayTypes;
    }
 
    @Override
    public Class<String[]> returnedClass() {
        return String[].class;
    }
 
    @Override
    public boolean equals(Object x, Object y) throws HibernateException {
        return x == null ? y == null : x.equals(y);
    }
 
    @Override
    public int hashCode(Object x) throws HibernateException {
        return x == null ? 0 : x.hashCode();
    }
 
    @Override
    public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
        // get the first column names
        if (names != null && names.length > 0 && rs != null && rs.getArray(names[0]) != null) {
            String[] results = (String[]) rs.getArray(names[0]).getArray();
            return results;
        }
        return null;
    }
 
    @Override
    public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
        // setting the column with string array
        if (value != null && st != null) {
            String[] castObject = (String[]) value;
            Array array = session.connection().createArrayOf("text", castObject);
            st.setArray(index, array);
        } else {
            st.setNull(index, arrayTypes[0]);
        }
    }
 
    @Override
    public Object deepCopy(Object value) throws HibernateException {
        return value == null ? null : ((String[]) value).clone();
    }
 
    @Override
    public boolean isMutable() {
        return false;
    }
 
    @Override
    public Serializable disassemble(Object value) throws HibernateException {
        return (Serializable) value;
    }
 
    @Override
    public Object assemble(Serializable cached, Object owner) throws HibernateException {
        return cached;
    }
 
    @Override
    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return original;
    }
}
 
cs


위와 같은 custom Type클래스를 지정해 줘야 JPA는 컬럼을 인식하고 값을 던져줄 수 있습니다.


현재 lib중에 위와같은 클래스를 구현한 것이 있습니다.


@Entity
@Table(name = "story")
public class Story {
 
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_story")
    @SequenceGenerator(name = "seq_story", sequenceName = "story_id_seq", allocationSize = 1)
    private Integer id;
 
    @Column(name = "title")
    private String title;
 
    @Column(name = "description")
    private String description;
 
    @Column(name = "foreword")
    private String foreword;
 
    @Column(name = "character")
    private String character;
 
    @Column(name = "tags")
    @Type(type = "com.iropke.common.hibernate.ArrayType")
    private String[] tags;
 
    @Column(name = "co_author")
    private String coAuthor;
 
    @Column(name = "cover_image")
    private String coverImage;
 
    @Column(name = "views",  columnDefinition = "Integer default 0")
    private Integer views;
 
    @Enumerated(EnumType.STRING)
    @Column(name = "state")
    private State state;
 
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "create_at", insertable = false, updatable = false)
    private Date createAt;
 
    @ManyToOne
    @JoinColumn(name="member")
    private Member member;
 
    @OneToMany
    @Where(clause = "state not in ('deleted')")
    @OrderBy(clause = "id asc")
    @JoinColumn(name = "story")
    private List<Chapter> chapterList;
 
    public Integer getId() {
        return id;
    }
 
    public void setId(Integer id) {
        this.id = id;
    }
 
    public String getTitle() {
        return title;
    }
 
    public void setTitle(String title) {
        this.title = title;
    }
 
    public String getDescription() {
        return description;
    }
 
    public void setDescription(String description) {
        this.description = description;
    }
 
    public String getForeword() {
        return foreword;
    }
 
    public void setForeword(String foreword) {
        this.foreword = foreword;
    }
 
    public String getCharacter() {
        return character;
    }
 
    public void setCharacter(String character) {
        this.character = character;
    }
 
    public String[] getTags() {
        return tags;
    }
 
    public void setTags(String[] tags) {
        this.tags = tags;
    }
 
    public String getCoAuthor() {
        return coAuthor;
    }
 
    public void setCoAuthor(String coAuthor) {
        this.coAuthor = coAuthor;
    }
 
    public String getCoverImage() {
        return coverImage;
    }
 
    public void setCoverImage(String coverImage) {
        this.coverImage = coverImage;
    }
 
    public Integer getViews() {
        return views;
    }
 
    public void setViews(Integer views) {
        this.views = views;
    }
 
    public State getState() {
        return state;
    }
 
    public void setState(State state) {
        this.state = state;
    }
 
    public Date getCreateAt() {
        return createAt;
    }
 
    public void setCreateAt(Date createAt) {
        this.createAt = createAt;
    }
 
    public Member getMember() {
        return member;
    }
 
    public void setMember(Member member) {
        this.member = member;
    }
 
    public List<Chapter> getChapterList() {
        return chapterList;
    }
 
    public void setChapterList(List<Chapter> chapterList) {
        this.chapterList = chapterList;
    }
 
}
 
cs



@Type(type = "com.iropke.common.hibernate.ArrayType")을 주목하시고 추가해 주시면 됩니다. 


3. 검색 구현하기


SearchController를 구현해 보도록 하겠습니다.

Comments