DevVeri.com

Boğulacaksan büyük veride boğul!

Java JPA ile MongoDB Kullanımı

Java ile MongoDB’yi kullanmak için bir çok yöntem mevcut, bunlardan birisi JPA üzerinden erişim. Hali hazırda JPA kullanıyorsanız veya NoSQL konusunda yeniyseniz bu yöntemi tercih edebilirsiniz.

MongoDB’nin JPA erişimini Datanucleus kütüphaneleri üzerinden kullanıyoruz. Datanucleus ismini özellikle GAE (Google App Engine) üzerinde uygulama geliştirenler bileceklerdir, GAE üzerinde veritabanı olarak Big Table kullanılıyor ve erişim de JPA ya da JDO üzerinden yapılıyor. Her iki kütüphane de Datanucleus tarafından geliştirilmiş. Bunlar dışında HBase, Amazon S3 kütüphaneleri de mevcut.

Bu yöntemin en net problemi konfigürasyon problemi. Uygulamayı ayağa kaldırabilmek için çok uğraşmam gerekti. Özellikle maven konfigürasyonunda hangi versiyonların uyumlu olduğunu bulmak ve plugin ayarları çok sıkıntılı. Ancak sonunda doğru formulü bularak çalıştırmayı başarabildim. Buradan dökümantasyonun da yetersiz olduğunu söyleyebilirim.

Örnek uygulamaya https://github.com/hakanilter/devveri üzerinden ulaşabilirsiniz.

Öncelikle gerekli maven repo’larını pom.xml’e eklemek gerekiyor:

<repositories>
    <repository>  
        <id>DN_M2_Repo</id>  
        <name>DataNucleus Repository</name>  
        <url>http://www.datanucleus.org/downloads/maven2</url>  
    </repository>
</repositories>

<pluginRepositories>  
    <pluginRepository>  
        <id>DataNucleus_2</id>  
        <url>http://www.datanucleus.org/downloads/maven2/</url>  
    </pluginRepository>  
</pluginRepositories>

Daha sonra gerekli kütüphaneleri dependency olarak eklememiz gerekiyor;

<dependencies>
    <dependency>
        <groupId>org.datanucleus</groupId>
        <artifactId>datanucleus-core</artifactId>
        <version>3.0.10</version>
    </dependency>
    <dependency>
        <groupId>org.datanucleus</groupId>
        <artifactId>datanucleus-api-jpa</artifactId>
        <version>3.0.10</version>
    </dependency>
    <dependency>
        <groupId>org.datanucleus</groupId>
        <artifactId>datanucleus-mongodb</artifactId>
        <version>3.0.6</version>
    </dependency>
    <dependency>
        <groupId>org.apache.geronimo.specs</groupId>
        <artifactId>geronimo-jpa_2.0_spec</artifactId>
        <version>1.1</version>
    </dependency>                
    <dependency>
        <groupId>javax.persistence</groupId>
        <artifactId>persistence-api</artifactId>
        <version>1.0</version>
    </dependency>
     <dependency>
        <groupId>javax.transaction</groupId>
        <artifactId>jta</artifactId>
        <version>1.1</version>
    </dependency>
    <dependency>
        <groupId>javax.jdo</groupId>
        <artifactId>jdo-api</artifactId>
        <version>3.0</version>
        <exclusions>
            <exclusion>
                <groupId>javax.transaction</groupId>
                <artifactId>transaction-api</artifactId>
            </exclusion>
        </exclusions>            
    </dependency>                                                
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>${slf4j.version}</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
        <version>${slf4j.version}</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>${slf4j.version}</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.15</version>
        <exclusions>
            <exclusion>
                <groupId>javax.mail</groupId>
                <artifactId>mail</artifactId>
            </exclusion>
            <exclusion>
                <groupId>javax.jms</groupId>
                <artifactId>jms</artifactId>
            </exclusion>
            <exclusion>
                <groupId>com.sun.jdmk</groupId>
                <artifactId>jmxtools</artifactId>
            </exclusion>
            <exclusion>
                <groupId>com.sun.jmx</groupId>
                <artifactId>jmxri</artifactId>
            </exclusion>
        </exclusions>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.8.2</version>
        <scope>test</scope>            
    </dependency>
</dependencies>

Bunların dışında en önemli kısım (birçok kişinin sorun yaşadığını tahmin ettiğim) enhance işlemi. Enhance işlemi derleme aşamasında proje içerisindeki entity sınıflarını tarayıp bunlar üzerinde bytecode seviyesinde değişiklik yaparak “persistable” hale getiriyor. Bu işlem yapılmaz ise çalışma esnasında hata alıyorsunuz. Bu yüzden bu işlemin derleme sırasında mutlaka yapılması gerekiyor. Bununla ilgili plugin ayarları ise şöyle;

<plugin>  
    <groupId>org.datanucleus</groupId>  
    <artifactId>maven-datanucleus-plugin</artifactId>  
    <version>3.0.1</version>  
    <configuration>  
        <log4jConfiguration>${basedir}/log4j.properties</log4jConfiguration>  
        <verbose>false</verbose>  
        <api>JPA</api>  
        <persistenceUnitName>example</persistenceUnitName>  
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.datanucleus</groupId>
            <artifactId>datanucleus-core</artifactId>
            <version>3.0.10</version>
        </dependency>
    </dependencies>                 
    <executions>  
        <execution>  
            <phase>compile</phase>  
            <goals>  
                <goal>enhance</goal>  
            </goals>  
        </execution>  
    </executions>  
</plugin>

Plugin düzgün ayarlanırsa derleme aşamasında şöyle bir çıktı olduğunu görebilirsiniz:

[ERROR] --------------------
[ERROR]  Standard error from the DataNucleus tool + org.datanucleus.enhancer.DataNucleusEnhancer :
[ERROR] --------------------
[ERROR] May 13, 2012 5:21:53 PM org.datanucleus.enhancer.DataNucleusEnhancer <init>
INFO: DataNucleus Enhancer : Using ClassEnhancer "ASM" for API "JPA"
May 13, 2012 5:21:54 PM org.datanucleus.enhancer.DataNucleusEnhancer main
INFO: DataNucleus Enhancer (version 3.0.1) : Enhancement of classes
May 13, 2012 5:21:54 PM org.datanucleus.metadata.MetaDataManager loadPersistenceUnit
WARNING: Class com.devveri.dao.MemberDAO was specified in persistence-unit example but not annotated, so ignoring
May 13, 2012 5:21:54 PM org.datanucleus.metadata.MetaDataManager loadPersistenceUnit
WARNING: Class com.devveri.dao.base.BaseDAO was specified in persistence-unit example but not annotated, so ignoring
May 13, 2012 5:21:54 PM org.datanucleus.enhancer.AbstractClassEnhancer save
INFO: Writing class file "/home/haqen/git/devveri/datanucleus-mongodb/target/classes/com/devveri/model/Member.class" with enhanced definition
May 13, 2012 5:21:54 PM org.datanucleus.enhancer.DataNucleusEnhancer addMessage
INFO: DataNucleus Enhancer completed with success for 1 classes. Timings : input=144 ms, enhance=69 ms, total=213 ms. Consult the log for full details

[ERROR] --------------------

persistence.xml dosyasında ise ayarlarımızın MongoDB’ye uygun olması için şu şekilde olması gerekiyor:

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
    version="1.0">

    <!-- Tutorial "unit" -->
    <persistence-unit name="example">
        <properties>
            <property name="datanucleus.ConnectionURL" value="mongodb:localhost:27001/example" />
            <property name="datanucleus.storeManagerType" value="mongodb" />
            <property name="datanucleus.autoCreateSchema" value="true" />
        </properties>
    </persistence-unit>

</persistence>

Bu ayarlarla işin zor kısmı halledilmiş oluyor. Bundan sonrası bildiğimiz JPA ile ilgili. Örnek bir Member sınıfını tanımlıyoruz:

package com.devveri.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity  
@Table(name="members")
public class Member
{
    @Id   
    @GeneratedValue(strategy=GenerationType.AUTO)  
    @Column(name="_id")  
    private String id;  

    private String fullName;
    private String email;
    private Integer age;
    private Double balance;

    public Member() {

    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getFullName() {
        return fullName;
    }

    public void setFullName(String fullName) {
        this.fullName = fullName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }

    @Override
    public String toString() {
        return "Member [id=" + id + ", fullName=" + fullName + ", email="
                + email + ", age=" + age + ", balance=" + balance + "]";
    }
}

Jenerik bir BaseDAO sınıfı oluşturarak içerisinde EntityManager yönetimini ve basit CRUD işlemlerini yazıyoruz:

package com.devveri.dao.base;

import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public abstract class BaseDAO<T, ID extends Serializable>
{
    private static final String PERSISTENCE_UNIT_NAME = "example";
    private static final Object lock = new Object();

    private final Class<T> persistentClass;

    private EntityManagerFactory emf;
    private EntityManager em;

    @SuppressWarnings("unchecked")
    public BaseDAO() {
        this.persistentClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }

    protected EntityManager getEntityManager()
    {
        if (em == null) {
            synchronized (lock) {
                if (em == null) {
                    emf = Persistence.createEntityManagerFactory(PERSISTENCE_UNIT_NAME);
                    em = emf.createEntityManager();                     
                }
            }
        }
        return em;
    }

    public void close()
    {
        if (em != null) {
            em.close();
            em = null;            
        }        
        if (emf != null) {
            emf.close();
            emf = null;
        }
    }

    public void save(final T t) {
        getEntityManager().persist(t);
    }

    public void delete(ID id)
    {
        T t = getEntityManager().getReference(persistentClass, id);
        if (t != null) {
            getEntityManager().remove(t);    
        }
    }

    public T update(T t) {
        return getEntityManager().merge(t);
    }    

    public T findById(ID id) {
        return getEntityManager().find(persistentClass, id);
    }

    @SuppressWarnings("unchecked")
    public List<T> findAll()
    {
        final String query = String.format("select t from %s t", persistentClass.getSimpleName());
        return getEntityManager().createQuery(query).getResultList();
    }

    @SuppressWarnings("unchecked")
    public List<T> findByProperty(String propertyName, Object value)
    {
        final String query = String.format("select t from %s t where %s = :parameter",
                persistentClass.getSimpleName(), propertyName);
        return getEntityManager().createQuery(query).setParameter("parameter", value).getResultList();
    }
}

Jenerik sınıfımızdan türeyen MemberDAO sınıfını yazıyoruz, içerisine özel birşey eklememize gerek yok:

package com.devveri.dao;

import com.devveri.dao.base.BaseDAO;
import com.devveri.model.Member;

public class MemberDAO extends BaseDAO<Member, String> {

}

Test kodumuzu da şöyle yazabiliriz:

package com.devveri.test;

import junit.framework.Assert;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.devveri.dao.MemberDAO;
import com.devveri.model.Member;

public class MongoJPATest
{
    private MemberDAO dao;

    @Before
    public void setUp() {
        dao = new MemberDAO();
    }

    @After
    public void tearDown()
    {
        if (dao != null) {
            dao.close();
        }
    }

    @Test
    public void test()
    {
        // save
        Member member = new Member();
        member.setFullName("John Doe");
        member.setEmail("johndoe@gmail.com");
        member.setAge(35);
        member.setBalance(10.5);
        dao.save(member);
        Assert.assertNotNull(member.getId());

        // find by id
        Member loaded = dao.findById(member.getId());
        System.out.println(loaded);
        Assert.assertNotNull(loaded);
        Assert.assertEquals(member.getId(), loaded.getId());
        Assert.assertEquals(member.getFullName(), loaded.getFullName());
        Assert.assertEquals(member.getEmail(), loaded.getEmail());
        Assert.assertEquals(member.getAge(), loaded.getAge());
        Assert.assertEquals(member.getBalance(), loaded.getBalance(), 0.01);

        // delete
        dao.delete(loaded.getId());
        Member deleted = dao.findById(loaded.getId());
        Assert.assertNull(deleted);
    }
}

JPA üzerinden erişimde id ile özel bir durum var. Member sınıfındaki gibi id alanını tanımladığımızda MongoDB tarafında hem _id alanı oluşturuluyor hem de id alanı. Biz kullanırken her zaman id üzerinden erişiyoruz. JPA _id alanını bizden izole ediyor. Örnek bir kayıt şu şekilde görünüyor:

> db.members.find()
{ "_id" : ObjectId("4fafc0686c0091c084d8570a"), "id" : "ff8080813746877601374687762f0000", "fullName" : "John Doe", "email" : "johndoe@gmail.com", "balance" : 10.5, "age" : 35 }

JPA ile MongoDB erişimi genel olarak bu şekilde. Kompleks sorgularda ya da join’lerde nasıl davrandığını biraz daha kurcalayarak incelemek gerekiyor.

, , ,

4 thoughts on “Java JPA ile MongoDB Kullanımı

  • fatih tekin dedi ki:

    Oncelikle yazınız icin cok teşekkurler emeginize saglık. NoSql i yeni tanımaya ve kullanılan teknolojileri henuz ogrenmeye basladım. Merak ettigim collectionlardaki herbir document ın sahip olabildigi alanlar dinamik olarak farklılasabiliyorken. neden Entity gibi runtime da degisemeyecek bir yoldan db ye erişim saglamam gerekir.

    • Hakan İlter dedi ki:

      Eğer ihtiyacınız olan şey dinamik bir collection yapısı ise JPA üzerinden ya da Spring Data üzerinden MongoDB’yi kullanmak çok mantıklı değil. Ancak veri yapınız nadiren değişiyorsa ve JPA kullanmaya aşinaysanız, mevcut JPA ile çalışan bir yapıyı veritabanından MongoDB’ye taşımak isterseniz bu şekilde kullanabilirsiniz diye düşünüyorum.

  • Halil dedi ki:

    böyle çalışması güzel ama bu sefer de mongodb kullanan ama sql mantığıyla oluşmuş bir database sahibi olunur ki mongonun yapısına ters olmaz mı ?

    • Hakan İlter dedi ki:

      Evet çok mantılı değil bence de. spring-data kullanıyoruz biz, onun da kendine özgü problemleri var. En güzeli native mongodb driver’ı kullanmak sanırım.

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

This site uses Akismet to reduce spam. Learn how your comment data is processed.