Spring Data 여러 기능

오늘은 Spring Data의 몇가지 기능들을 살펴 볼 예정이다. 예전에 작성했던 Spring data common 기타 기능과는 별개로 유용하게 사용할 수 있는 것들을 정리해 보도록 하자. 물론 어떤 특정한 버전, 프로젝트들은 따로 명시를 해놓도록 하겠다.

query method

Spring data 프로젝트의 특징인 query method는 아주 유용한 기능이다. 물론 복잡한 쿼리에는 사용할 수 없지만 간단한 쿼리를 작성하는데는 더할 나위 없이 유용한 기능이다.
필자도 간단한 쿼리를 작성할 때는 자주 이용하고 있다. 잘 모르고 있을 수도 있는 기능들을 좀 더 살펴보자.

public interface PersonRepository extends CrudRepository<Person, String> {

    List<Person> findByName(String name);

}

만약 이름으로 select을 하고 있다면 대부분 위와 같이 작성을 할 것이다. find란 키워드와 by라는 키워드 spring data에서 정의한 키워드이다. 만약 두개의 키워드가 존재하지 않다면 쿼리메서드는 제대로 동작하지 않을 수 있다.

하지만 select 할때는 find 라는 키워드 말고도 여러 키워드들이 존재한다. 굳이 find 키워드를 사용할 필요 없이 다른 키워드를 사용해도 된다.

public interface PersonRepository extends CrudRepository<Person, String> {

    List<Person> findByName(String name);

    List<Person> queryByName(String name);

    List<Person> streamByName(String name);

    List<Person> getByName(String name);

    List<Person> readByName(String name);

}

find 라는 키워드 말고도 query, stream, get, read 등으로 대체 할 수 있다.
이외에도 카운터를 세는 count 존재 여부를 알려주는 exists 등이 있다.

또한 삭제쿼리 메서드도 존재하는데 remove, delete 두가지 키워드를 사용해서 삭제할 수 있다.

public interface PersonRepository extends CrudRepositor<Person, String> {

    void deleteByName(String name);

    void removeByName(String name);

}

위와 같이 사용해도 동일한 결과를 얻을 수 있다.

find 라는 키워드 말고도 여러 키워드가 있으니 다른 키워드도 사용해도 좋다.

사실 위와 같이 delete method 에는 @Modifying 어노테이션이 필요 없다. @Modifying 어노테이션은 @Query(update or delete) 어노테이션을 사용할 때만 작성하면 된다.

또한 한글, 중국어, 일어 등도 가능하다. 사실 그럴일은 거의거의 없지만 그냥 가능하다고 만 알고 있자.

//@Entity, //Document ...
public class Person {

  // ...

  private String 이름;

  //... getter setter

}

public interface PersonRepository extends CrudRepositor<Person, String> {

  List<Book> findBy이름(String name);

}

entityName (Data-JPA)

이것은 Jpa에 특화된 기능이다. 다른 data 하위 프로젝트엔 동작하지 않는다. @Query 어노테이션을 사용할 때 유용하게 사용될 수 있는 기능이다.

entityName 이라는 키워드를 통해 해당 엔티티를 조회, 저장, 삭제등을 할 수 있다.

@MappedSuperclass
public class Product {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    ....
}

// book, disc 

@NoRepositoryBean
public interface ProductRepository<T extends Product> extends JpaRepository<T, Long> {

    @Query("select p from #{#entityName} p where p.name = :name")
    List<T> findName(String name);
}


public interface BookRepository extends ProductRepository<Book> {

}

public interface DiscRepository extends ProductRepository<Disc> {

}

주로 사용하고 있는 곳으로는 위와 같이 공통적인 상위 인터페이스를 정의 한 후 공통된 쿼리들을 entityName을 이용하여 처리 할 수 있다.
@Query 어노테이션을 자주 사용하는 분들은 아주 유용한 기능인 듯 싶다.

Mongo (Data-Mongo)

Spring data mongo를 사용할 때 몇가지 유용한 기능이다.

Document(model) 를 작성할 때 굳이 @Document 어노테이션과 @Id를 선언하지 않아도 된다.

@Value
public class Person {

    ObjectId id;

    String name;

    int age;

}

public interface PersonRepository extends MongoRepository<Person, ObjectId> {

}

위와 같이 Person 모델에 @Document 를 붙이지 않아도 자동으로 Spring data가 만들어서 넣어 준다. 만약 @Document 없다면 클래스명(첫글자는 소문자)으로 collection이 만들어 진다.

@Id도 마찬가지로 id, _id 라는 필드가 존재하면 그것을 키로 잡아 저장을 한다.

만약 어노테이션이 많아 보기가 힘들다면 작성하지 않아도 되며 명시적인 어노테이션을 선호한다면 작성해도 무방하다.

단, 아래코드는 동작하지 않는다.


@Value public class Person { ObjectId id; String name; int age; } public interface PersonRepository extends CrudRepository<Person, String> { }

그 이유는 해당 엔티티가 어느 스토어를 사용하지는 알수 없어 빈으로 등록하지 못한다. 위의 경우엔 명시적으로 @Document 어노테이션을 사용하거나 직접적인 MongoRepository를 사용해야 한다.

다른 spring data 하위 프로젝트(JPA는 해당사항 없다.) 들은 어떻게 되어 있는지 확인해보지 않았다. 만약 다른 프로젝트도 이런 기능이 있을 수도, 없을 수도 있으니 잘 보고 적용해야 된다.

Spring data 쪽에는 엔티티를 위와 같이 불변으로 만들어도 된다. (사실.. 확인은 mongo, redis 만 했다. 이것 또한 JPA는 해당사항 없다.)

JPA는 명시된 스펙으로 작성을 해야 되기 때문에 @Entity, @Id, 기본생성자, 정의된 스펙에 맞게 작성해야 한다.

public class Person {

    private final ObjectId id;

    private final String name;

    private final int age;

    public Person(ObjectId id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
   // getter
}

or

@Value
public class Person {

    ObjectId id;

    String name;

    int age;

}

or

@Value(staticConstructor = "of")
public class Person {

    ObjectId id;

    String name;

    int age;

}

@Value 는 롬복에 있는 어노테이션이며 여기를 참고하며 되겠다. 또한 팩토리 메서드를 만들어 사용해도 된다. 다른 더 많은 기능이 있으나 주로 사용될만한 기능은 이정도 일듯 싶다.

Streamable

Spring data 2.0 에서 추가된 Streamable 인터페이스를 query method 를 사용할 때 리턴타입으로 사용할 수 있다.

public interface PersonRepository extends MongoRepository<Person, String> {

    Streamable<Person> findByName(String name);

}

해당 인터페이스를 사용하면 바로 map, filter를 사용할 수 있다.

 List<String> names = people.map(Person::getName)
                    .stream()
                    .collect(Collectors.toList());

사실 사용 Api를 보면 List를 사용하는 것과 동일하다. 어차피 Stream으로 다시 만들어 하기에 굳이 사용할 필요가 있나 싶기도 하다.

하지만 spring data 2.2 부터는 (현재기준으로 아직 릴리즈는 되지 않았다) 조금 더 간편하게 사용할 수 있다.

List<String> names = people.map(Person::getName)
                    .toList()

Set<String> names = people.map(Person::getName)
                    .toSet()

내부적인 코드는 동일하지만 사용하는 Api는 간단하게 사용할 수 있다.

또한 spring data 2.2 부터는 좀 더 커스텀한 Wrapper Type의 Streamable 만들어 사용할 수 있다.

//@RequiredArgsConstructor(staticName = "of")
@RequiredArgsConstructor
public class Persons implements Streamable<Person> {

    private final Streamable<Person> people;

    public int getAge() {
        return people.stream()
                .mapToInt(Person::getAge)
                .sum();
    }

    @Override
    public Iterator<Person> iterator() {
        return people.iterator();
    }
}


public interface PersonRepository extends MongoRepository<Person, String> {

    Persons readByName(String name);

}

Persons people = personRepository.readByName("wonwoo");
int totalAge = people.getTotalAge();

이렇게 Wrapper Type을 만들어 내부적으로 기능들을 추가할 수 있어 유용한 기능인 것 같다.

오늘은 이렇게 Spring data 쪽에 사용되는 유용한 기능들을 살펴봤다. 조금이라도 도움이 되었으면 좋겠다.

오랜만에 포스팅을 했다. 이런저런 일도 있고 사실 귀찮았던게 더 컷다. 일주일에 한번(적어도 이주일엔)은 꼭 쓰도록 노력해야겠다.

mongodb 간단하게 해보기.

설치는 인터넷이 자세히 나와 있으니 참고 하길 바란다.
맥 경우에는 brew으로 간편하게 설치도 가능하니 참고하길 바란다.
https://docs.mongodb.com/manual/tutorial/install-mongodb-on-os-x/

우리는 간단하게 커멘드 창에서 CUD를 해보겠다. 몽고 서버와 몽고 클라이언트를 실행시키자. select는 공부할게 많으니 나중에 다시 해보겠다.

insert

> db.user.insert({ name: "wonwoo", age : 27, email: "test@test.com" } )
WriteResult({ "nInserted" : 1 })

nosql 이니 json으로 들어간다.

> db.user.insertOne({ name: "wonwoo1", age : 20, email: "test1@test.com" } )
{
    "acknowledged" : true,
    "insertedId" : ObjectId("5767f41228dcf51e4b235173")
}

insertOne 도 insert가 가능하다. 다른점은 잘 모르겠다. 리턴이 다른건 확인이 가능한데.. 흠..

> db.user.insertMany([
  { name: "wonwoo2", age : 22, email: "test2@test.com"}, 
  { name: "wonwoo3", age : 32, email: "test3@test.com"}, 
  { name: "wonwoo4", age : 37, email: "test4@test.com"}])

{
    "acknowledged" : true,
    "insertedIds" : [
        ObjectId("5767f58128dcf51e4b235174"),
        ObjectId("5767f58128dcf51e4b235175"),
        ObjectId("5767f58128dcf51e4b235176")
    ]
}

다중 insert도 가능하다. insertMany를 이용하면 된다.

update

몽고 사이트에서 예제로 있는 것을 해보자.

db.users.insertMany(
   [
     {
       _id: 1,
       name: "sue",
       age: 19,
       type: 1,
       status: "P",
       favorites: { artist: "Picasso", food: "pizza" },
       finished: [ 17, 3 ],
       badges: [ "blue", "black" ],
       points: [
          { points: 85, bonus: 20 },
          { points: 85, bonus: 10 }
       ]
     },
     {
       _id: 2,
       name: "bob",
       age: 42,
       type: 1,
       status: "A",
       favorites: { artist: "Miro", food: "meringue" },
       finished: [ 11, 25 ],
       badges: [ "green" ],
       points: [
          { points: 85, bonus: 20 },
          { points: 64, bonus: 12 }
       ]
     },
     {
       _id: 3,
       name: "ahn",
       age: 22,
       type: 2,
       status: "A",
       favorites: { artist: "Cassatt", food: "cake" },
       finished: [ 6 ],
       badges: [ "blue", "Picasso" ],
       points: [
          { points: 81, bonus: 8 },
          { points: 55, bonus: 20 }
       ]
     },
     {
       _id: 4,
       name: "xi",
       age: 34,
       type: 2,
       status: "D",
       favorites: { artist: "Chagall", food: "chocolate" },
       finished: [ 5, 11 ],
       badges: [ "Picasso", "black" ],
       points: [
          { points: 53, bonus: 15 },
          { points: 51, bonus: 15 }
       ]
     },
     {
       _id: 5,
       name: "xyz",
       age: 23,
       type: 2,
       status: "D",
       favorites: { artist: "Noguchi", food: "nougat" },
       finished: [ 14, 6 ],
       badges: [ "orange" ],
       points: [
          { points: 71, bonus: 20 }
       ]
     },
     {
       _id: 6,
       name: "abc",
       age: 43,
       type: 1,
       status: "A",
       favorites: { food: "pizza", artist: "Picasso" },
       finished: [ 18, 12 ],
       badges: [ "black", "blue" ],
       points: [
          { points: 78, bonus: 8 },
          { points: 57, bonus: 7 }
       ]
     }
   ]
)

업데이트를 해보자.

> db.users.updateOne(
...    { "favorites.artist": "Picasso" },
...    {
...      $set: { "favorites.food": "pie", type: 3 },
...      $currentDate: { lastModified: true }
...    }
... )
{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }

favorites 안에 artist의 명이 Picasso인 것을 favorites.foods는 pie로 바꾸고 type은 3으로 업데이틀 한다. 그리고 lastModified를 현재 시간으로 업데이트 한다.

> db.users.findOne({"_id" : 1})
{
    "_id" : 1,
    "name" : "sue",
    "age" : 19,
    "type" : 3,
    "status" : "P",
    "favorites" : {
        "artist" : "Picasso",
        "food" : "pie"
    },
    "finished" : [
        17,
        3
    ],
    "badges" : [
        "blue",
        "black"
    ],
    "points" : [
        {
            "points" : 85,
            "bonus" : 20
        },
        {
            "points" : 85,
            "bonus" : 10
        }
    ],
    "lastModified" : ISODate("2016-06-20T14:04:50.252Z")
}

findOne으로 아이디가 1인것을 찾으면 이렇게 업데이트가 되어 있다. 근데 이상하다 id가 6인 것도 favorites.artist 똑같이 Picasso 인데 업데이트 되지 않았다. 모두다 업데이트 하고 싶다면 updateMany를 사용하면 된다.

> db.users.updateMany(
...    { "favorites.artist": "Picasso" },
...    {
...      $set: { "favorites.artist": "Pisanello", type: 3 },
...      $currentDate: { lastModified: true }
...    }
... )
{ "acknowledged" : true, "matchedCount" : 2, "modifiedCount" : 2 }

modifiedCount가 2인 것을 확인할 수 있다. 다시 확인 해보자.

> db.users.findOne({"_id" : 6})
{
    "_id" : 6,
    "name" : "abc",
    "age" : 43,
    "type" : 3,
    "status" : "A",
    "favorites" : {
        "food" : "pizza",
        "artist" : "Pisanello"
    },
    "finished" : [
        18,
        12
    ],
    "badges" : [
        "black",
        "blue"
    ],
    "points" : [
        {
            "points" : 78,
            "bonus" : 8
        },
        {
            "points" : 57,
            "bonus" : 7
        }
    ],
    "lastModified" : ISODate("2016-06-20T14:15:04.696Z")
}

아디디가 6인 것도 변경 되었다.

delete

delete 도 간단하다.

> db.users.deleteOne( { status: "D" } )
{ "acknowledged" : true, "deletedCount" : 1 }

deleteOne을 이용하여 status 가 “D” 인 것을 삭제 하였다. update와 마찬가지로 처음 나오는 것만 삭제하고 나머지는 삭제 하지 않는다. 모두 삭제 하고 싶다면 아래와 같이 deleteMany를 이용하면 된다.

> db.users.deleteMany({ status : "A" })
{ "acknowledged" : true, "deletedCount" : 3 }

deleteMany를 이용하여 status가 “A”인 것을 모두 삭제 하였다.

db.users.find()

위와 같이 find()를 이용하여 확인해보면 status가 “A”인 것은 모두 삭제 되었다.

> db.users.remove({})
WriteResult({ "nRemoved" : 6 })

> db.users.deleteMany({})
{ "acknowledged" : true, "deletedCount" : 6 }

remove와 deleteMany를 이용하여 모두 삭제 할 수 있다. 이것 역시 다른 점이 있다면 리턴타입이 다르다. 그 이외는 잘 모르겠다.

현재 진행한 CUD 말고 API가 몇개 더 있지만 자주 사용하는 것만 작성했다. 더 궁금하다면 레퍼런스를 참고 하길 바란다.
다음시간에는 select의 대해서 알아보자. 뭘 하든 select는 양이 많으니 ….

이상하게 위의 update 데이터중 커멘드 창에서 6개를 한꺼번에 넣으니까 에러가 발생한다. 커멘드라 찾기도 귀찮아서 나눠서 넣었다.

예제 )

db.users.insertMany(
   [
     {
       _id: 1,
       name: "sue",
       age: 19,
       type: 1,
       status: "P",
       favorites: { artist: "Picasso", food: "pizza" },
       finished: [ 17, 3 ],
       badges: [ "blue", "black" ],
       points: [
          { points: 85, bonus: 20 },
          { points: 85, bonus: 10 }
       ]
     },
     {
       _id: 2,
       name: "bob",
       age: 42,
       type: 1,
       status: "A",
       favorites: { artist: "Miro", food: "meringue" },
       finished: [ 11, 25 ],
       badges: [ "green" ],
       points: [
          { points: 85, bonus: 20 },
          { points: 64, bonus: 12 }
       ]
     },
     {
       _id: 3,
       name: "ahn",
       age: 22,
       type: 2,
       status: "A",
       favorites: { artist: "Cassatt", food: "cake" },
       finished: [ 6 ],
       badges: [ "blue", "Picasso" ],
       points: [
          { points: 81, bonus: 8 },
          { points: 55, bonus: 20 }
       ]
     }
  ]
)
db.users.insertMany(
   [
     {
       _id: 4,
       name: "xi",
       age: 34,
       type: 2,
       status: "D",
       favorites: { artist: "Chagall", food: "chocolate" },
       finished: [ 5, 11 ],
       badges: [ "Picasso", "black" ],
       points: [
          { points: 53, bonus: 15 },
          { points: 51, bonus: 15 }
       ]
     },
     {
       _id: 5,
       name: "xyz",
       age: 23,
       type: 2,
       status: "D",
       favorites: { artist: "Noguchi", food: "nougat" },
       finished: [ 14, 6 ],
       badges: [ "orange" ],
       points: [
          { points: 71, bonus: 20 }
       ]
     },
     {
       _id: 6,
       name: "abc",
       age: 43,
       type: 1,
       status: "A",
       favorites: { food: "pizza", artist: "Picasso" },
       finished: [ 18, 12 ],
       badges: [ "black", "blue" ],
       points: [
          { points: 78, bonus: 8 },
          { points: 57, bonus: 7 }
       ]
     }
   ]
)