RevelFramework — 4) GORM + MySQL 5.7 JSON Column Marshalling

MySQL 5.7 부터는 PostrgreSQL처럼 컬럼타입으로 JSON을 사용할 수 있습니다. 규모가 크지 않은 초기 서비스 단계라면 굳이 RDBMS와 NoSQL을 나누지 않고도 JSON 컬럼을 적절히 사용해서 아주 멋진 데이터 저장소를 만들 수 있다고 생각이되서 JSON 컬럼을 많이 활용하는 편입니다.

  1. 해당 컬럼에 인덱스가 필요한가?
  2. 해당 컬럼이 집계성 컬럼이거나, 조인이 필요한가?
  3. 해당 컬럼이 WHERE 절에 자주 등장하는가?

저는 위 3가지에 해당되지 않으면 왠만해서는 데이터를 JSON 컬럼에 저장합니다. 데이터로써의 의미보다 메타정보(참고용 정보)로써 화면에서 쓰이는 정보성 역할이 더 크다고 생각하기 때문에, 경험상 추후 확장성을 고려해 JSON으로 저장하는 것이 더 많은 이점이 있더군요. 또한 비즈니스 로직을 처리하는 과정에서도 저런 값들은 관련성이 매우 적기도 했고요. (물론 로직 처리 중 필요할 경우에는 JSON 파싱을 통해서 해당 데이터를 사용 할 수도 있습니다.)

기존에 Spring + Hibernate + JPA 조합에서는 MySQLJSONUserType 이라고하는 Hibernate의 ParameterizedType과 JSONMySQL57Dialect를 만들어서 JSON 필드를 Jackson JsonNode로 매핑하도록 처리했었습니다.

public class MySQLJSONUserType implements ParameterizedType, UserType {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final ClassLoaderService classLoaderService = new ClassLoaderServiceImpl();
public static final String JSON_TYPE = "json";
public static final String CLASS = "CLASS";
private Class jsonClassType;
@Override
public Class<Object> returnedClass() {
return Object.class;
}
@Override
public int[] sqlTypes() {
return new int[]{Types.JAVA_OBJECT};
}
@Override
public Object nullSafeGet(ResultSet resultSet, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
try {
if (resultSet.getBytes(names[0]) != null) {
final String json = new String(resultSet.getBytes(names[0]), "UTF-8");
return objectMapper.readValue(json, jsonClassType);
}
return null;
} catch (IOException e) {
throw new HibernateException(e);
}
}
@Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
try {
final String json = value == null ? null : objectMapper.writeValueAsString(value);
st.setObject(index, json);
} catch (JsonProcessingException e) {
throw new HibernateException(e);
}
}
@Override
public void setParameterValues(Properties parameters) {
final String clazz = (String) parameters.get(CLASS);
jsonClassType = classLoaderService.classForName(clazz);
}
@SuppressWarnings("unchecked")
@Override
public Object deepCopy(Object value) throws HibernateException {
if (!(value instanceof Collection)) {
return value;
}
Collection<?> collection = (Collection) value;
Collection collectionClone = CollectionFactory.newInstance(collection.getClass());
collectionClone.addAll(collection.stream().map(this::deepCopy).collect(Collectors.toList()));
return collectionClone;
}
static final class CollectionFactory {
@SuppressWarnings("unchecked")
static <E, T extends Collection<E>> T newInstance(Class<? extends Collection> collectionClass) {
if (List.class.isAssignableFrom(collectionClass)) {
return (T) new ArrayList<E>();
} else if (Set.class.isAssignableFrom(collectionClass)) {
return (T) new HashSet<E>();
} else {
throw new IllegalArgumentException("Unsupported collection type : " + collectionClass);
}
}
}
@Override
public boolean isMutable() {
return true;
}
@Override
public boolean equals(Object x, Object y) throws HibernateException {
if (x == y) {
return true;
}
if ((x == null) || (y == null)) {
return false;
}
return x.equals(y);
}
@Override
public int hashCode(Object x) throws HibernateException {
assert (x != null);
return x.hashCode();
}
@Override
public Object assemble(Serializable cached, Object owner) throws HibernateException {
return deepCopy(cached);
}
@Override
public Serializable disassemble(Object value) throws HibernateException {
Object deepCopy = deepCopy(value);
if (!(deepCopy instanceof Serializable)) {
throw new SerializationException(String.format("%s is not serializable class", value), null);
}
return (Serializable) deepCopy;
}
@Override
public Object replace(Object original, Object target, Object owner) throws HibernateException {
return deepCopy(original);
}
}
public class JSONMySQL57Dialect extends MySQL57InnoDBDialect {
public JSONMySQL57Dialect() {
super();
registerColumnType(Types.JAVA_OBJECT, MySQLJSONUserType.JSON_TYPE);
}
}
같은 방식으로 GO와 GORM에서는 다음과 같이 처리할 수 있습니다.
먼저 다음과 같이 JSON 타입을 생성하고
package models
import (
"bytes"
"errors"
"database/sql/driver"
)
type JSON []byte
func (j JSON) Value() (driver.Value, error) {
if j.IsNull() {
return nil, nil
}
return string(j), nil
}
func (j *JSON) Scan(value interface{}) error {
if value == nil {
*j = nil
return nil
}
s, ok := value.([]byte)
if !ok {
errors.New("Invalid Scan Source")
}
*j = append((*j)[0:0], s...)
return nil
}
func (m JSON) MarshalJSON() ([]byte, error) {
if m == nil {
return []byte("null"), nil
}
return m, nil
}
func (m *JSON) UnmarshalJSON(data []byte) error {
if m == nil {
return errors.New("null point exception")
}
*m = append((*m)[0:0], data...)
return nil
}
func (j JSON) IsNull() bool {
return len(j) == 0 || string(j) == "null"
}
func (j JSON) Equals(j1 JSON) bool {
return bytes.Equal([]byte(j), []byte(j1))
}
package models
type Company struct {
MasterCompCd string `xorm:"column:MASTER_COMP_CD" json:"masterCompCd"`
CompCd string `gorm:"column:COMP_CD" json:"compCd"`
CompNm string `gorm:"column:COMP_NM" json:"compNm"`
CompanyJson JSON `gorm:"column:COMPANY_JSON" json:"companyJson"`
ZipCode string `gorm:"column:ZIP_CODE" json:"zipCode"`
UseYn string `gorm:"column:USE_YN" json:"useYn"`
Audit
}
func (Company) TableName() string {
return "COMP_M"
}

해당 JSON 컬럼을 생성한 JSON 타입으로 지정하기만 하면 됩니다.
이제 다음과 같이 JSON 컬럼에 있는 값들이 JSON으로 잘 표현됩니다~~
Screen Shot 2016-08-29 at 2.44.08 PM
One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.